Batch script: Pass returned GUID as URL parameter for desktop shortcut - windows

I'm trying to create desktop shortcuts to a private page we work with that will open in Edge, direct to a specific URL, and pass the GUID as a URL parameter.
I've tried the following but as you can expect, only the string "powershell" is passed on to the URL, not the returned GUID.
SET a=powershell -Command "[guid]::NewGuid().ToString()"
C:\Windows\System32\cmd.exe /c start msedge "https://www.website.com/page?user="%a% --no-first-run
How can I replace the %a% portion of the URL with the returned contents of the system GUID?
powershell -Command "[guid]::NewGuid().ToString()"

Note:
This answer addresses the question as asked: it captures the output from a PowerShell command in a batch-file variable for later use in the same batch file.
Alternatively, the specific task at hand can also be performed in full by a single PowerShell command, as shown in zett42's helpful answer.
Batch files (executed by cmd.exe) have no concept of a what is known as command substitution in POSIX-compatible shells (a feature that PowerShell itself provides too, though it has no official name there): the ability to assign a command's output to a variable.[1]
Instead, you must use a for /f loop to capture command output in a variable (which generally loops over each output line, but in your case there is only one output line):
#echo off
setlocal
:: Capture the output from a PowerShell command in variable %guid%, via
:: a for /f loop:
for /f "usebackq delims=" %%a in (`powershell -Command "[guid]::NewGuid().ToString()"`) do set "guid=%%a"
:: Note: No need for `cmd /c` from a batch file to use `start`
start "" msedge "https://www.website.com/page?user=%guid%" --no-first-run
Note: setlocal, while not strictly necessary, localizes any variable definitions to the batch file at hand.
Run for /? in a cmd.exe session for help.
This answer discusses using for /f to capture command output in more detail; notably:
usebackq isn't strictly necessary here, but is generally advisable to give you the freedom to use both ' and " quoting in the command line being invoked.
Similarly, delims= isn't strictly necessary here, since the output by definition contains no spaces, but it is generally advisable if the intent is to capture an output line in full.
The "" as the first start argument isn't strictly necessary here, but in general it is useful when invoking applications whose paths must be double-quoted. Without "" as the first argument, a double-quoted application path would be interpreted as starts window-title argument (which only meaningfully applies to console applications).
[1] In POSIX-compatible shells, use $(...) (e.g, captured="$(whoami)"). In PowerShell, you can use the command -as-is as the RHS of the assignment (e.g., $captured = whoami)

It is possible to do all of this directly using a PowerShell one-liner:
powershell -noprofile -command start msedge \"https://www.website.com/page?user=$(New-Guid) --no-first-run\"
Passing -noprofile to powershell.exe is most of the time a good idea to reduce startup time and provide a more predictable environment as no user profile will be loaded.
start is an alias for the Start-Process command.
Here start gets passed two positional arguments, the name of the process to start (-FilePath parameter) and the process's arguments as a single string (-ArgumentList parameter). Therefore, the 2nd argument must be quoted. To pass the quotes from the command processor cmd.exe through to PowerShell, they must be backslash-escaped.
Within the process's parameter string, the subexpression operator $(…) is used to call the New-Guid command inline and convert it to a string (by implicitly calling the .ToString() method of the Guid object it returns).
If you actually need to use the GUID as a variable in other parts of your batch script (which is not clear from the question), then this helpful answer provides a solution.

Related

How can i use a cmd variable in a powershell command?

I have created a simple .bat file. In this batch file i have a variable named urlExample which is equal to "example.com".
In the same batch file i want to use this variable urlExample in a powershell command.
Specifically, consider the following code:
#echo off
set urlExample = "example.com"
powershell -ExecutionPolicy Bypass -Command "& {$WebClient = New-Object System.Net.WebClient;$WebClient.DownloadFile($urlExample,"C:\.....somePath")
How could i achieve using the urlExample inside the WebClient command?
P.s. I don't want to simply put the url in the DownloadFile's first argument. I want to pass it with a batch variable.
Thanks in advance
In cmd.exe, all variables are also environment variables, such as %urlExample% in your case, and child processes - such as a call to powershell.exe, the Windows PowerShell CLI, inherit environment variables.
By contrast, PowerShell also has shell(-only) variables (e.g., $urlExample, limited to that session only), whereas environment variables must be accessed via the env namespace (e.g. $env:urlExample - see the conceptual about_Environment_Variables help topic).
While you can use string interpolation on the cmd.exe side to "bake in" the values of cmd.exe-defined environment variables, by embedding %urlExample% in the -Command argument, it is more robust to let PowerShell access the environment variable, by referencing $env:urlExample.
Therefore:
#echo off
:: Note: No spaces around "=", double-quote the name *and* the value.
set "urlExample=example.com"
:: Note the reference to $env:urlExample
:: Embedded " chars. are escaped as \"
powershell -ExecutionPolicy Bypass -Command "$WebClient = New-Object System.Net.WebClient; $WebClient.DownloadFile($env:urlExample, \"C:\.....somePath\")"
Note:
-ExecutionPolicy Bypass isn't strictly needed in this case, given that no execution of a script file is (.ps1) is involved here (whether directly with -File or indirectly, as part of a -Command argument).
However, given that the effective execution policy also applies in less obvious scenarios when you use -Command (the default parameter of powershell.exe)[1] - such as (possibly implicitly) loading a module that is either a script module (*.psm1) and / or contains formatting / type-definition data (*.ps1xml) - using -ExecutionPolicy Bypass is a good habit to form to ensure predictable execution, assuming that you trust the code you're invoking.
As Compo points out, another good habit to form to ensure a predictable execution environment is to use -NoProfile, which bypasses loading of PowerShell's profile files. In addition to preventing potentially unnecessary / unwanted modifications of the execution environment by the profiles, bypassing profile loading also speeds up the command.
[1] Note that pwsh, the PowerShell (Core) CLI, now defaults to -File.

Pass argument from Powershell to Batch script

I need to pass a password with special characters from powershell script automation.ps1 to batch script batch_script.bat which pipes it to main.py. Piping from batch_script.bat to main.py works fine, that is authentication succeeds. However, when I run the entire procedure described above, authentication fails, but echoing the password shows the correct password string.
My guess is that there are issues with special characters. What is a safe way to pass these strings?
Background
I want to automate the daily download from some external source via a Python script main.py. This process requires a password. So I wrote a batch_script.bat which pipes the password to the Python script when prompted for it. However, I don't want to store the password as plain text in the batch script, so I encrypted the password and wrote another layer automation.ps1 which decrypts the password and passes it as plain text to batch_script.bat.
automation.ps1
# get password
$securePassword = Get-Content "<file_path>" | ConvertTo-SecureString
$credentials = New-Object System.Management.Automation.PsCredential("<user_name>",$securePassword)
$unsecurePassword = ($credentials).GetNetworkCredential().Password
# call script
$script = "<path>\batch_script.bat"
$args = #("`"<user name>`"","`"$unsecurePassword`"")
start-process $script $args
batch_script.bat
(I am aware that in this example I discard the passed username, just wanted to preserve the fact that I pass multiple arguments in case there is any relevance to it)
#echo off
SET username=%~1
SET password=%~2
echo(%password%|python main.py
With following, all special characters should be handled very well. If any character required to be escaped, check this
$pass could be any string but check for special characters of powershell
$pass="%^&<>|'\``,;=(\)![]\/";
# Wait till it ends with -Wait when using -NoNewWindow.
# It may be comprehensible to use `" instead of "" to denote we are enclosing string in quotes.
(thanks #mklement0 for elaboration).
start-process -Wait -NoNewWindow .\script.cmd "`"$pass`""
script.cmd
setlocal
rem Remove Double quotes
set "arg=%~1"
rem Test result with base64 encoding
echo|set/p="%arg%"|openssl base64
rem echo is used with set/p to prevent trailing new line.
echo|set/p="%arg%"|python main.py
rem Test with following, argument is in double quotes
rem script "%^&<>|'\`,;=(\)![]\/"
rem Expected result
rem %^&<>|'\`,;=(\)![]\/
tl;dr:
Unless you specifically need the batch file to run in a new window, avoid Start-Process (whose built-in aliases are start and saps), and invoke the batch file directly.
To avoid problems with special characters in $unsecurePassword, do not pass it as an argument, pass it via stdin (the standard input stream), which your batch file will pass through to your python script:
automation.ps1:
# ...
$script = "<path>\batch_script.bat"
# Pass the password via *stdin*
$unsecurePassword | & $script 'userName'
Note: It is the $OutputEncoding preference variable that controls what character encoding PowerShell uses for sending text to an external program's stdin. In Windows PowerShell, that variable defaults to ASCII(!) encoding, meaning that any characters outside the 7-bit ASCII-range of Unicode characters, such as accented characters, are unsupported (they turn to literal ?); fortunately, PowerShell [Core] v6+ now defaults to UTF-8. Assign the required encoding to $OutputEncoding as needed.
batch_script.bat:
#echo off
SET username=%~1
REM The batch file will pass its own stdin input through to Python.
python main.py
Read on for background information.
Invoking a batch file from PowerShell:
Unless you truly need to launch a batch file in a new window, the best approach is to invoke it directly from PowerShell; that way, it runs:
in the same console window, synchronously.
with its output streams connected to PowerShell's (which allows you to capture or redirect the output).
Because your batch-file path is stored in a variable, direct invocation requires use of &, the call operator:
# Note: The " chars. around $unsecurePassword are only needed if the variable
# value contains cmd.exe metacharacters - see next section.
& $script 'userA' `"$unsecurePassword`"
Start-Process is usually the wrong tool for invoking console applications, batch files, and other console-based scripts; see this answer for more information.
If you do need the batch file to run in a new window (which is only an option on Windows), use Start-Process as follows (the command will execute asynchronously, unless you also pass -Wait):
# The string with the arguments to pass is implicitly bound
# to the -ArgumentList parameter. Use only " for embedded quoting.
Start-Process $script "userA `"$unsecurePassword`""
Note: While the (implied) -ArgumentList (-Args) parameter is array-valued ([string[]]) and passing the arguments individually is arguably the cleaner approach, this generally does not work properly, due to a longstanding bug that probably won't get fixed; for instance,
Start-Process foo.exe -Args 'one', 'two (2)' passes 3 arguments rather than 2; that is, it passes single string 'two (2)' as two arguments - see this GitHub issue.
Therefore, it is ultimately simpler and more predictable to pass a single argument with embedded quoting to -ArgumentList, but be sure to use only " (not ') for embedded quoting:
Start-Process foo.exe -Args "one `"two (2)`""
Passing arguments robustly to cmd.exe / batch files:
Note:
The limitations of cmd.exe (the legacy command processor that interprets batch files) prevent fully robust solutions; notably, you cannot prevent the interpretation of tokens such as %foo% as environment-variable references when you call a batch file from PowerShell (at least not without altering the argument to %foo^%, which will retain the ^).
In your specific case, since you're trying to echo an argument unquoted, embedded double quotes (") in such an argument - which need to be escaped as "" - aren't properly supported: they are passed through as "".
Passing an unquoted argument to cmd.exe / a batch file breaks, if that argument contains one of cmd.exe's metacharacters, i.e., characters with special syntactic meaning; in this context, they are: & | < > ^ "
The solution is to enclose the argument in double quotes ("..."), with the added need to double " chars. that are embedded (a part of the value).
PowerShell, after performing its own parsing of the command line (notably evaluating variable references and expressions), constructs the command line that is ultimately used to invoke the external target program, behind the scenes.
However, it only automatically double-quotes an argument if it contains spaces, not if it only contains cmd.exe metacharacters; e.g., a variable with verbatim string content two (2) is passed double-quoted - $val = 'two 2'; .\foo.bat $val results in command line .\foo.bat "two 2" - whereas string content a&b is not - $val = 'a&b'.\foo.bat $val results in .\foo.bat a&b - which breaks.
The solution - as shown in your question - is to enclose the variable reference in literal, embedded " characters, because such a "pre-quoted" value instructs PowerShell to pass the value as-is:
$val = 'a&b'; .\foo.bat `"$val`" results in .\foo.bat "a&b"
Note: .\foo.bat "`"$val`"" has the same effect; I'm taking advantage of the fact that PowerShell in argument (parsing) mode (generally) implicitly treats arguments as if they were double-quoted; in expression (parsing) mode, such as in the array-construction statement in the question (#(..., ...)), you do need the "`"$val`"" form.
The problem with your specific batch file:
A properly "..."-enclosed argument (with any embedded " chars. escaped as "") is properly seen as a parameter (e.g., %1) inside a batch file.
However, it is seen with the enclosing double quotes and with any doubled embedded " chars.
If you were to pass this parameter to the target program (python in this case) as an argument, everything would work as expected.
However, since you're passing the value via stdin using echo, you need to strip the enclosing double quotes so that they're not passed as part of the value, which is what your batch file attempts (e.g., %~2)
However, passing the stripped value causes the echo command to break.
There is no good solution to this problem with echo, short of performing cumbersome explicit ^-escaping (^ being cmd.exe's escape character):
$escapedUnsecurePassword = $unsecurePassword -replace '[&|<>^]' -replace '"', '""'
& $script 'userA' `"$escapedUnsecurePassword`"
That alone still isn't enough, however - your batch_script.bat file needs a modification too:
Because the assignment itself in your SET password=%~2 command isn't protected with double quotes, it breaks with values that contain metacharacters; somewhat paradoxically, you must use the form SET "password=%~2" in order to safely strip the embedded enclosing " chars.:
#echo off
REM Strip the enclosing "..." from the arguments (%~<n>)
REM !! The *assignment itself* must be in "..." so that
REM !! it does not break if the value has cmd.exe metacharacters.
set "username=%~1"
set "password=%~2"
echo(%password%|python main.py
Note that that will work as intended for all metacharacters except the - of necessity doubled - embedded ", which are passed through as "".
However, there is a workaround for echoing a string with metacharacters unquoted, as also demonstrated in subcoder's helpful answer:
If you define batch_script.bat as follows:
#echo off
set "username=%~1"
REM Do NOT strip quotes from the password argument
set password=%2
REM Using a trick with set /p, echo the password unquoted.
REM Note: Put the "|" directly after the ":" to avoid trailing spaces.
<NUL set /p=%password% & echo:|python main.py
The workaround repurposes set's /p option, which accepts a prompt message to print when interactively prompting the user for a value and prints the message without quotes; the actual interactive prompt is suppressed via <NUL, so that only the message is printed.
echo: prints a newline (line break), which is necessary, because the set /p command prints its message without a trailing newline (if you don't want to send a newline, simply omit & echo:).
Caveat: In addition to the problem with embedded "" applying here too, this workaround has a side effect: it trims leading whitespace; e.g., " foo " results in output foo   (only trailing whitespace is preserved); however, given that arguments with leading whitespace are rare, this may not matter in practice.
Given how cumbersome / obscure the above approaches are, the stdin-based approach shown at the top is preferable.
You pass arguments to batch files in powershell using the -argumentlist switch of start/saps. For you you could use:
saps "$script" -argumentlist $args
But I would suggest first breaking $args up as it may not work since to pass arguments, you usually want to pass the arguments one at a time like:
saps "$script" -argumentlist "1","2","3"
Passing $args will work most of the time, but there are some cases where where it won't work. Most of the time you are fine though

proper way to remove double quotes from string in batch

I've got a batch script (app1.bat) calling another batch script (app2.bat) which itself calls a program in windows (program.exe).
app2.bat calls program.exe with a parameter after a flag in this way:
program.exe -f Parameter with whitespaces coming into the program
What I want to do is to pass the phrase that comes to program.exe from app1.bat into app2.bat but i don't know how to properly handle the doublequotes. Currently I am passing the phrase from app1.bat to app2.bat in double quotes and inside an app2.bat (prior to executing program.exe) I get rid of the quotes like that:
inside app1.bat
call app2.bat "Parameter with whitespaces coming into the program"
inside app2.bat
set old_phrase=%1%
set new_phrase=%old_phrase:"=%
program.exe -f %new_phrase%
old_phrase is
"Parameter with whitespaces coming into the program"
and new_phrase I end up with is
Parameter with whitespaces coming into the program
Is there any standard way to handle such a situation (being passing a string to an external program which expects a tring without quotes and being ok with whitespaces, whereas batch does not allow for no-quotes-and-whitespaces strings)
When you execute call /? from cmd to launch the help you will see quite a bit around expansion of %n
The first one states:
%~1 - expands %1 removing any surrounding quotes (")
You can therefore dump all the other set commands and simply run this in your batch file:
program.exe -f %~1

What is `cmd /s` for?

The Windows command prompt (cmd.exe) has an optional /s parameter, which modifies the behavior of /c (run a particular command and then exit) or /k (run a particular command and then show a shell prompt). This /s parameter evidently has something to do with some arcane quote handling.
The docs are confusing, but as far as I can tell, when you do cmd /csomething, and the something contains quotation marks, then by default cmd will sometimes strip off those quotes, and /s tells it to leave them alone.
What I don't understand is when the quote removal would break anything, because that's the only time /s ("suppress the default quote-removal behavior") would be necessary. It only removes quotes under a certain arcane set of conditions, and one of those conditions is that the first character after the /c must be a quotation mark. So it's not removing quotes around arguments; it's either removing quotes around the path to the EXE you're running, or around the entire command line (or possibly around the first half of the command line, which would be bizarre).
If the path to the EXE is quoted, e.g. cmd /c "c:\tools\foo.exe" arg1 arg2, then quotes are unnecessary, and if cmd wants to remove them, fine. (It won't remove them if the path has a space in the name -- that's another of the arcane rules.) I can't imagine any reason to suppress the quote removal, so /s seems unnecessary.
If the entire command line is quoted, e.g. cmd /c "foo.exe arg1 arg2", then it seems like quote removal would be a necessity, since there's no EXE named foo.exe arg1 arg2 on the system; so it seems like opting out of quote removal using /s would actually break things. (In actual fact, however, it does not break things: cmd /s /c "foo.exe arg1 arg2" works just fine.)
Is there some subtlety to /s that's eluding me? When would it ever be necessary? When would it even make any difference?
Cmd /S is very useful as it saves you having to worry about "quoting quotes". Recall that the /C argument means "execute this command as if I had typed it at the prompt, then quit".
So if you have a complicated command which you want to pass to CMD.exe you either have to remember CMD's argument quoting rules, and properly escape all of the quotes, or use /S, which triggers a special non-parsing rule of "Strip first and last " and treat all other characters as the command to execute unchanged".
You would use it where you want to take advantage of the capabilities of the CMD shell, rather than directly calling another program. For example environment variable expansion, output or input redirection, or using CMD.exe built-ins.
Example:
Use a shell built-in: This executes as-if you had typed DEL /Q/S "%TMP%\TestFile" at the prompt:
CMD.exe /S /C " DEL /Q/S "%TMP%\TestFile" "
This executes SomeCommand.exe redirecting standard output to a temp file and standard error to the same place:
CMD.exe /S /C " "%UserProfile%\SomeCommand.exe" > "%TMP%\TestOutput.txt" 2>&1 "
So what does /S give you extra? Mainly it saves you from having to worry about quoting the quotes. It also helps where you are unsure whether for example an environtment variable contains quote characters. Just say /S and put an extra quote at the beginning and end.
Vaguely Related: $* in Bourne Shell.
Some background
Recall that the list of arguments to main() is a C-ism and Unix-ism. The Unix/Linux shell (e.g. Bourne Shell etc) interprets the command line, un-quotes the arguments, expands wildcards like * to lists of files, and passes a list of arguments to the called program.
So if you say:
$ vi *.txt
The vi command sees for example these arguments:
vi
a.txt
b.txt
c.txt
d.txt
This is because unix/linux operates internally on the basis of "list of arguments".
Windows, which derives ultimately from CP/M and VAX, does not use this system internally. To the operating system, the command line is just a single string of characters. It is the responsibility of the called program to interpret the command line, expand file globs (* etc) and deal with unquoting quoted arguments.
So the arguments expected by C, have to be hacked up by the C runtime library. The operating system only supplies a single string with the arguments in, and if your language is not C (or even if it is) it may not be interpreted as space-separated arguments quoted according to shell rules, but as something completely different.
Here's an example of how it can make a difference.
Suppose you have two executables: c:\Program.exe and c:\Program Files\foo.exe.
If you say
cmd /c "c:\Program Files\foo"
you'll run foo.exe (with no arguments) whereas if you say
cmd /s /c "c:\Program Files\foo"
you'll run Program.exe with Files\foo as the argument.
(Oddly enough, in the first example, if foo.exe didn't exist, Program.exe would run instead.)
Addendum: if you were to type
c:\Program Files\foo
at the command prompt, you would run Program.exe (as happens with cmd /s /c) rather than foo.exe (as happens with just cmd /c). So one reason for using /s would be if you want to make sure a command is parsed in exactly the same way as if it were being typed at the command prompt. This is probably more likely to be desirable in the scenario in the question Michael Burr linked to, where cmd.exe is being launched by CreateProcess rather than from a batch file or the command line itself..
That is, if you say
CreateProcess("cmd.exe", "cmd /s /c \"" MY_COMMAND "\"", ...)
then the string MY_COMMAND will be parsed exactly as if it were typed at the command prompt. If you're taking command-line input from the user, or if you're a library processing a command line provided by an application, that's probably a good idea. For example, the C runtime library system() function might be implemented in this way.
In all but one specific case, the /S won't actually make any difference.
The help for cmd.exe is accurate, if a bit complicated:
If /C or /K is specified, then the remainder of the command line after
the switch is processed as a command line, where the following logic is
used to process quote (") characters:
If all of the following conditions are met, then quote characters
on the command line are preserved:
no /S switch
exactly two quote characters
no special characters between the two quote characters,
where special is one of: &<>()#^|
there are one or more whitespace characters between the
two quote characters
the string between the two quote characters is the name
of an executable file.
Otherwise, old behavior is to see if the first character is
a quote character and if so, strip the leading character and
remove the last quote character on the command line, preserving
any text after the last quote character.
I'd summarize as follows:
Normal behavior:
If the rest of the command line after /K or /C starts with a quote, both that quote and the final quote are removed. (See exception below.) Other than that, no quotes are removed.
Exception:
If the rest of the command line after /K or /C starts with a quote, followed by the name of an executable file, followed by another quote, AND if those are the only two quotes, AND if the file name contains spaces but contains no special characters, then the quotes are not removed (even though they normally would have been removed according to the rule above).
The only effect of /S is to override this one exception, so that the two quote characters are still removed in that case.
If you always use /S, you can forget about the exception and just remember the "normal" case. The downside is that cmd.exe /S /C "file name with spaces.exe" argument1 won't work without adding an extra set of quotes, whereas without /S it would have worked... until you decide to replace argument1 with "argument1".

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