I want to be able to set the %ERRORLEVEL% environment variable (also referred to as a "system variable") to an arbitrary value in the beginning of a command prompt script (i.e. cmd /c) running in PowerShell. Whenever people want to set %ERRORLEVEL% (1), they do the following:
cmd /c "exit /b 3"
cmd /c "echo %ERRORLEVEL%"
However, although %ERRORLEVEL% is set to 3 when the above is run in the normal Command Prompt, if these exact lines are executed in PowerShell, the exit code environment variable is not set to 3 and remains 0. In addition, you cannot do the following:
cmd /v:on /c "exit /b 3 & echo !ERRORLEVEL!"
The exit command breaks completely out of the command execution (i.e. cmd /c) and no other command after the & is run.
Therefore, I tried executing the following command in PowerShell:
cmd /v:on /c "SET %ERRORLEVEL% = 4 & echo !ERRORLEVEL!"
The expected output is 4, but this always outputs 0. I cannot figure out why I am unable to SET the %ERRORLEVEL% environment variable. I have used delayed command execution (example here), but no amount of fiddling seems to work here.
Does anyone have any idea of why the command SET %ERRORLEVEL% = 4 does not work?
If you are not able SET environment variables, then how can set %ERRORLEVEL% to an arbitrary value in a string of commands separated by an ampersand (&) like above?
If you want to store the eventual exit code in a cmd.exe variable for later use with exit, do not try to set ERRORLEVEL; instead, use a custom variable name; e.g., ec (for exit code):
# Execute in PowerShell
PS> cmd /v /c 'set "ec=3" & echo ... & exit /b !ec!'; $LASTEXITCODE
...
3
On the cmd.exe side:
set "ec=3" sets variable ec to value 3
Enclosing the name-value pair in "..." isn't strictly necessary, but clearly delineates the end of the value, and allows use of values with embedded special characters such as &.
The variable's value can later be referenced as %ec% (up-front expansion) or !ec! (delayed expansion, if enabled via setlocal enabledelayedexpansion or via command-line switch /v - see help setlocal)
echo ... is a sample command representing further commands
exit /b !ec! exits with the value of variable ec as the exit code; note how the variable is of necessity referenced as !ec! rather than %ec%, via delayed expansion, because the variable was set as part of the same statement.
Whether you use /b (exit the current batch file only) or not doesn't make a difference here; either way, the exit statement in this case determines the process exit code of the cmd instance.
The commands are sequenced (unconditionally executed one after the other), with the & operator, as part of a single statement.
On the PowerShell side:
PowerShell's analog to cmd.exe's & is ;, the statement separator - it allows you to place multiple statements on a single line.
$LASTEXITCODE is the automatic PowerShell variable that contains the process exit code of the most recently executed external program, which in this case is cmd.exe's, with value 3.
Does anyone have any idea why the command SET %ERRORLEVEL% = 4 does not work?
To summarize the helpful information from the comments on the question:
Fundamentally, do no try to set the dynamic (pseudo) environment variable %ERRORLEVEL% - it is automatically maintained by cmd.exe to reflect the most recent command's exit code - see the bottom section.
Do not put spaces around = in a cmd.exe variable assignment:
The space(s) before the = become part of the variable name.
The space(s) after become part of the value.
Do not enclose the target variable name in %...% (unless you want to indirectly set a variable, via another variable whose value contains the name of the variable to assign to).
set %ERRORLEVEL% = 4
With %ERRORLEVEL% reflecting 0 at the start of your command sequence, the above assignment creates a variable literally named 0 (that is, the value of %ERRORLEVEL% followed by a single space), whose value is 4 (that is, a single space followed by 4).
Variables in cmd.exe:
Fundamentally, with the exceptions discussed below, variables in cmd.exe are all environment variables.
Unlike in shells such as Bash and PowerShell, there is no separate namespace for shell-local variables that are not seen by child processes.
This has the following implications:
Predefined persistent environment variables such as %Path% share the same namespace as custom variables you define with the SET command, so you have to be mindful of name collisions.
Similarly, child processes you run from your cmd.exe session / batch file inherit custom variables you've created in the session.
Note that the use of setlocal does not change that; setlocal is a cmd-internal scoping mechanism that allows you to control the lifetime of custom environment variables, by localizing them to the scope (batch file) in which setlocal was called, and removing them via a subsequent endlocal call or, implicitly, at the end of the enclosing batch file.
Custom variables are process-only environment variables that go out of scope with the cmd.exe process; persistent environment variable definitions must be created and modified via the registry, such as with the setx.exe utility.
In addition to the predefined (persistent) environment variables and custom (session-only) environment variables, cmd.exe maintains dynamic pseudo environment variables such as %ERRORLEVEL% and %RANDOM% (see list below):
These pseudo environment variables have dynamic values maintained by cmd.exe itself...
... and they are not visible to child processes.
Note: Strictly speaking, these dynamic variables are only enabled with the so-called command extensions turned on, but that is true by default (you can disable them with /E:OFF, but that is ill-advised).
Because these dynamic variables are not strictly part of the process environment that child processes inherit a copy of, they are not environment variables, even though help SET somewhat confusingly calls them dynamic environment variables.
You shouldn't (and cannot) modify these variables.
If you try, what really happens is that you shadow (override) these pseudo variables with real, custom environment variables, which by definition have static values.
Later code that relies on the variables by that name to have their usual, dynamic behavior can therefore malfunction.
The list of dynamic variables, as retrieved on Windows 10 via help SET (emphasis added):
If Command Extensions are enabled, then there are several dynamic
environment variables that can be expanded but which don't show up in
the list of variables displayed by SET. These variable values are
computed dynamically each time the value of the variable is expanded.
If the user explicitly defines a variable with one of these names, then
that definition will override the dynamic one described below:
%CD% - expands to the current directory string.
%DATE% - expands to current date using same format as DATE command.
%TIME% - expands to current time using same format as TIME command.
%RANDOM% - expands to a random decimal number between 0 and 32767.
%ERRORLEVEL% - expands to the current ERRORLEVEL value
%CMDEXTVERSION% - expands to the current Command Processor Extensions
version number.
%CMDCMDLINE% - expands to the original command line that invoked the
Command Processor.
%HIGHESTNUMANODENUMBER% - expands to the highest NUMA node number
on this machine.
Related
1. Problem
I have a complicated batch file where some parts need to run with elevated/admin rights (e.g. interacting with Windows services) and I found a Powershell way to do that:
powershell.exe -command "try {$proc = start-process -wait -Verb runas -filepath '%~nx0' -ArgumentList '<arguments>'; exit $proc.ExitCode} catch {write-host $Error; exit -10}"
But there's a huge caveat! The elevated instance of my script (%~nx0) starts with a fresh copy of environment variables and everything I set "var=content" before is unavailable.
2. What I've tried so far
This Powershell script doesn't help either because Verb = "RunAs" requires UseShellExecute = $true which in turn is mutually exclusive to/with StartInfo.EnvironmentVariables.Add()
$p = New-Object System.Diagnostics.Process
$p.StartInfo.FileName = "cmd.exe";
$p.StartInfo.Arguments = '/k set blasfg'
$p.StartInfo.UseShellExecute = $true;
$p.StartInfo.Verb = "RunAs";
$p.StartInfo.EnvironmentVariables.Add("blasfg", "C:\\Temp")
$p.Start() | Out-Null
$p.WaitForExit()
exit $p.ExitCode
And even if that would work I'd still need to transfer dozens of variables...
3. unappealing semi-solutions
because circumventing the problem is no proper solution.
helper tools like hstart - because I can't relay on external tools. Only CMD, Powershell and maybe VBscript (but it looks like runas plus wait and errorlevel/ExitCode processing isn't possible with/in vbs).
passing (only required) variables as arguments - because I need dozens and escaping them is an ugly chore (both the result and doing it).
restarting the whole script - because it's inefficient with all the parsing, checking processing and other tasks happening again (and again and ...). I'd like to keep the elevated parts to a minimum and some actions can later be run as a normal user (e.g service start/stop).
Writing the environment to a file and rereading it in the elevated instance - because it's an ugly hack and I'd hope there's a cleaner option out there. And writing possibly sensitive information to a file is even worse than storing it temporarily in an environment variable.
Here's a proof of concept that uses the following approach:
Make the powershell call invoke another, aux. powershell instance as the elevated target process.
This allows the outer powershell instance to "bake" Set-Item statements that re-create the caller's environment variables (which the outer instance inherited, and which can therefore be enumerated with Get-ChilItem Env:) into the -command string passed to the aux. instance, followed by a re-invocation of the original batch file.
Caveat: This solution blindly recreates all environment variables defined in the caller's process in the elevated process - consider pre-filtering, possibly by name patterns, such as by a shared prefix; e.g., to limit variable re-creation to those whose names start with foo, replace Get-ChildItem Env: with Get-ChildItem Env:foo* in the command below.
#echo off & setlocal
:: Test if elevated.
net session 1>NUL 2>NUL && goto :ELEVATED
:: Set sample env. vars. to pass to the elevated re-invocation.
set foo1=bar
set "foo2=none done"
set foo3=3" of snow
:: " dummy comment to fix syntax highlighting
:: Helper variable to facilitate re-invocation.
set "thisBatchFilePath=%~f0"
:: Re-invoke with elevation, synchronously, reporting the exit
:: code of the elevated run.
:: Two sample arguments, ... and "quoted argument" are passed on re-invocation.
powershell -noprofile -command ^
trap { [Console]::Error.WriteLine($_); exit -667 } ^
exit ( ^
Start-Process -Wait -PassThru -Verb RunAs powershell ^
"\" -noprofile -command `\" $(Get-ChildItem Env: | ForEach-Object { 'Set-Item \\\"env:' + $_.Name + '\\\" \\\"' + $($_.Value -replace '\""', '`\\\""') + '\\\"; ' }) cmd /c '\`\"%thisBatchFilePath:'=''%\`\" ... \`\"quoted argument\`\" & exit'; exit `$LASTEXITCODE`\" \"" ^
).ExitCode
echo -- Elevated re-invocation exited with %ERRORLEVEL%.
:: End of non-elevated part.
exit /b
:ELEVATED
echo Now running elevated...
echo -- Arguments received:
echo [%*]
echo -- Env. vars. whose names start with "foo":
set foo
:: Determine the exit code to report.
set ec=5
echo -- Exiting with exit code %ec%...
:: Pause, so you can inspect the output before exiting.
pause
exit /b %ec%
Note:
trap { [Console]::Error.WriteLine($_); exit -667 } handles the case where the user declines the elevation prompt, which causes a statement-terminating error that the trap statement catches (using a try / catch statement around the Start-Process call is also an option, and usually the better choice, but in this case trap is syntactically easier).
Specifying pass-through arguments (arguments to pass directly to the re-invocation of the (elevated) batch file, after the cmd /c '\`\"%thisBatchFilePath:'=''%\`\" part above):
If arguments contain ', you must double them ('')
If arguments need double-quoting, you must enclose them in '\`\"...\`\" (sic), as shown with \`\"quoted argument\`\" above.
The cmd /c '<batch-file> & exit' re-invocation technique is required to ensure robust exit-code reporting, unfortunately - see this answer for details.
The explicit exit $LASTEXITCODE statement after the batch-file re-invocation is required to make the PowerShell CLI report the specific exit code reported by the batch file - without that, any nonzero exit code would be mapped to 1. See this answer for a comprehensive discussion of exit codes in PowerShell.
A piping hot solution
Derived from mklement0's working proof of concept and Jeroen Mostert's Base64 suggestion I've built a solution with this approach:
Pipe in data from inside the batch to the outer Powershell.
Let it convert the piped data into a Base64 string.
which is passed on into the command line of the elevated Powershell.
which in turn converts it back and pipes it into the new batch's instance.
It is more flexible because you're not limited to environment variables (you can essentially pass on anything (text based)) and the Powershell command doesn't need to be edited to choose what gets piped through. But it has a few limitations mklement0's implementation doesn't suffer from:
Environment variables containing newlines will not be passed on correctly and can cause chaos (depending on what comes after the LF, see barz).
Currently every line piped through (except for the first one) gets one whitespace prepended to it (so far I couldn't figure out how to fix that). It's usually not a problem and can be worked around (fooDoubleQouting is a negative example).
the elevated instance doesn't react to console input as usual any more (see notes).
Example / test batch:
#echo off & setlocal EnableDelayedExpansion
::# Test if elevated.
net session 1>NUL 2>NUL && goto :ELEVATED
(set LF=^
%=this line is empty=%
)
::# Set sample env. vars. to pass to the elevated instance.
set foo1=bar
set "foo2=none done"
set foo3=3" of snow
set "barz= Line1!LF! foo1=Surprise^! foo1 isn't '%foo1%' anymore. It was unintentionally overwritten."
set barts=1 " 3_consecutive_" "_before_EOL
set "barl=' sdfs' ´´`` =43::523; \/{[]} 457wb457;; %%%^!2^!11^!^!"
::# ' dummy comment#1 to fix syntax highlighting.
::# Helper variable to facilitate re-invocation (in case %~f0 contains any single quotes).
set "_selfBat=%~f0"
::# DDE - so "!" don't get expanded anymore. Was only needed for "set barz=..."
setlocal DisableDelayedExpansion
::# print variables etc. to console before self invocation & elevation.
call :testPrint
::# Generate pipe input. Be aware of CMD's handicaps of whats allowed in a command block.
::# eg. "REM" is not allowed and neither is echoing an unescaped closing parenthesis: ")" -> "^^^)"
(
echo[foo_Setting_one=extra-varval.
set ^^"
echo[bar_stuff=in between.^^^)^^^"
set bar
echo["fooDoubleQouting=testertest"
) | powershell.exe -nologo -noprofile -command ^
trap { [Console]::Error.WriteLine($_); exit -667 } ^
exit ( ^
Start-Process -PassThru -Wait -WindowStyle Maximized -Verb RunAs 'powershell.exe' ^
"\"-nol -nop -comm `\" $('Write-Output $([Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(\\\"' + $([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($(foreach ($i in $input) {\"$i`n\"})))) + '\\\")))') | cmd.exe '/D','/U','/T:4F','/S','/C',' \`\"%_selfBat:'=''%\`\" \`\"quoted argument\`\" nonQtdArg & exit'; exit `$LastExitCode `\" \"" ^
).exitCode
echo[
echo[ ---- Returned errorlevel is: %ERRORLEVEL%
pause
endlocal & endlocal & exit /b %ERRORLEVEL%
:testPrint
echo[
echo[ ---- WhiteSpaceTest: "%barts%"
set foo
set bar
echo[
set ^"
exit /B
::# " dummy comment#2 to fix syntax highlighting again.
:ELEVATED
setlocal DisableDelayedExpansion
::# Read and parse piped in data.
::# (with "delims" & "eol" truly defined as empty so every line is read as-is, even empty lines)
for /F delims^=^ eol^= %%A in ('findstr.exe "^"') do (
echo[ Parsing %%~A
for /F "tokens=1,* delims=="eol^= %%B in ("%%~A") do (
echo[ into "%%~B"
echo[ equals "%%~C"
::# Convert the piped in data back into environment variables+values.
set "%%~B=%%~C" 2>NUL
)
echo[
)
echo[-------- END PIPEREADING --------
echo[-- Arguments received:
echo[ [%*]
call :testPrint
set "ERR=42"
echo[
::# to actually pause and/or wait for / react to user input(!) one needs to pipe in CON (console).
<con set /P ERR=Enter arbitrary exitcode / errorlevel:
endlocal & exit /B %ERR%
Notes:
see mklement0's notes.
The CMD /C '<batch-file_withEscaped'> & exit' re-invocation technique isn't required if you consistently exit /b X in your batch file. Then &\`\"%_selfBat%\`\" instead of CMD /C ... & exit is enough (with separately separated arguments: 'arg1','arg2').
'/D','/T:4F', - Ignore CMD's registry AutoRun commands and set fore-/background colors to white on dark red.
echo[ instead of echo is safer and quicker (cmd doesn't need to search for actual executables named echo.*).
<con is required in the elevated instance for anything needing user interaction (eg. pause or set /P ...). Without <con the now empty(?) piped in standard input (pipe#0) still delivers nul(?) to anything asking for it (my assumption). I'm sure there is a way too rescue stdin from the pipe and reattach it to con (maybe some kinde of breakthrou from in here).
barl's backticks get mangled.
Escaping hell
Here are the dynamic middle and inner command lines to show whats going on and shave away some escaping magic:
powershell.exe -nol -nop -comm "Write-Output $([Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(\"<<BASE64_BLOB>>\"))) | cmd.exe '/D','/U','/T:4F','/S','/C',' \"<<path\this.cmd_withEscaped'>>\" \"quoted argument\" nonQtdArg & exit'; exit $LastExitCode"
Even with just my default environment that command line is somewhere around 5kB big(!), thanks to the <<BASE64_BLOB>>.
cmd.exe /D /U /T:4F /S /C " "<<path\this.cmd_withNormal'>>" "quoted argument" nonQtdArg & exit"
So yeah, like has been said, the environment is not meant to be passed from a user to another, by design, because of the security implications. It doesn't mean that it can't be done, even if it's not something you're "supposed" to do. While I do think you should look into what do you actually want to achieve, I absolutely hate the type of answers where people tell you what you achsually "should" do and not answer the actual question at all. So I'm giving you both options here.
"Passing" the environment
You have several options here
From the elevated child process, read the environment variables from the unelevated caller parent process' memory using the NtQueryInformationProcess and ReadProcessMemory APIs.
Then either overwrite the variables on the target process (in your case, the current process) with WriteProcessMemory or just set them as you normally would. You can achieve this with only Powershell, albeit you need to add some C# code to call the required API functions.
Here in my Enable-Privilege.ps1 example you can see how to implement NtQueryInformationProcess in PowerShell and manipulate a parent process (I wrote it for the purpose of modifying privilege tokens inside self/parent/any process). You would actually want to use a part of it because you need to enable SeDebugPrivilege to be able to manipulate memory of other processes.
This option would probably be most "clean" and robust solution without any instantly obvious caveats. See this codeproject article for more information: Read Environment Strings of Remote Process
Inside the unelevated parent process, iterate through all the environment variables and write them as a string to a single environment variable. Then pass that single environment value as an argument when spawning the elevated child process, where you can parse that string and write those environment values back. You would likely run to the same caveats as option 3 here though.
Pipe the variables from the parent to the child, like has been proposed here already. The problem here is that the batch processor is really finicky and the rules of parsing and escaping are super janky, so it's very likely you would run to issues with special characters and other similar caveats with this option.
Using a kernel-mode driver, overwrite the security token of the unelevated process with a elevated one, writing back the original token after you are done. On the surface this would seem like the perfect solution, since you could actually stay inside the previously-unelevated process and retain it's environment without changing context, only the security context would be replaced. As in kernel-mode you can modify everything, the security tokens are simple memory structs inside kernel memory which you can change. The problem with this approach is that it completely bypasses the windows security model, as it's supposed to be impossible to change the security token of an existing process. Because it's supposed to be "impossible", it goes deep into undocumented territory and inside the kernel you can easily break stuff if you don't know what you're doing, so this is definitely the "advanced" option (even though this particular thing is not too complicated, it's basically just writing some memory). As it's something you're not supposed to be doing, there is a possibility it breaks something since Windows does not expect a process to suddenly have a different security context. That being said, I've used this approach with no problems in the past. It could be broken in the future though by any change in the security design. You would also need to either enable testsigning (aka Disable Driver Signature Enforcement), digitally sign your driver or use some other method to bypass this requirement (f.ex. through a hypervisor or an exploit), but that is out of the scope of this answer.
The achsually version
"because circumventing the problem is no proper solution."
In this case, I would do exactly that. Since your problem is of such nature that a easy solution for it doesn't exist, because it's not supported by design. It's hard to propose a specific solution since the lack of information of what it is you're actually trying to achieve here.
I'm gonna try to cover this in a general sense. First is to think about the what it is you're actually trying to achieve here part. What are the operations you need to do which require elevation? There would be multiple ways to achieve whatever it is in a supported fashion.
Examples:
For whatever you need to read/write/modify, you could change the security security settings of the target (instead of the source). Meaning that let's say you need to access a specific registry key, service, file, folder, whatever, you could simply modify the ACL of the target to allow the source (i.e. the user) to do whatever operation you need. If you need to modify a single service for example, you could add the start/stop/modify right for only that single process.
If the thing you need is specific to the types of operations rather than specific targets, you could add the required privileges to the "Users" group. Or make a new group with the required privileges, and then add the user to that group.
If you want more granular control on what can/can't be done and/or the operations are specific, you could write a simple program and run it as a elevated service. Then you could just tell that service to do the required operations from the unelevated batch script, so no requesting elevation and spawning new process would be needed. You could simply do my-service.exe do-the-thing from batch, and that my-service would do the operation you need.
You could also always ask for the elevation in beginning of the script, but as it's clear you don't want to do this with full administrator rights, you could create a new user for just this purpose which you add to a new group for it which has the required privileges you need. Note that without resorting to the aforementioned kernel-mode ""hacks"", you cannot add new privileges for a user on-the-fly, only enable/disable/remove existing ones. What you can do though is add them beforehand based on what you need, but that will need to happen before the process is started.
There is a script.sh file
set FABRIC_CFG_PATH=<some path>
set CORE_PEER_LOCALMSPID=<some id>
If I'm running this script in windows, the env variables are not getting set.
Whereas if setting the env using the cmd approach,
E.g., on windows cmd
set FABRIC_CFG_PATH=<some path>
It works fine.
So how can I set the env in windows through a shell script file?
Since your intent is to define current-process-only environment variables (rather than persistently defined ones, which on Windows are stored in the registry) you need to use a script file / batch file that runs in-process in order for environment variables defined therein to be seen by the script's caller.
Therefore:
If the caller is a cmd.exe session, you must use a batch file: a plain-text file with filename extension .cmd (or, less preferably, .bat[1]) that uses cmd.exe syntax.
If the caller is a PowerShell session, you must use a PowerShell script: a plain-text file with filename extension .ps1 that uses PowerShell syntax.
Note: While you can call a .cmd file (batch file) from PowerShell too (but not directly vice versa), this will not work as intended, because of necessity it runs in a (cmd.exe) child process, whose environment variables aren't seen by the PowerShell caller.
As for .sh files: they have no predefined meaning on Windows, but may be defined by third-party applications, such as Git Bash. In the case of the latter, invoking a .sh file passes it to the POSIX-compatible Bash shell, which has its own syntax. More importantly, invoking such a file won't work as intended when called from either cmd.exe or PowerShell, because Bash must run in a child process, and child processes cannot set environment variables for their parents.
cmd.exe / batch-file example:
Create a file named envVars.cmd, for instance, and place the following lines in it:
#echo off
:: Note: Do NOT use `setlocal` here
set "FABRIC_CFG_PATH=C:\path\to\some directory\config"
set "CORE_PEER_LOCALMSPID=42"
Then, from your cmd.exe session / another batch file, call the file as follows to make the environment variable-definitions take effect for the current process (assuming the file is in the current directory):
.\envVars.cmd
You will then able to refer to the newly defined variables as %FABRIC_CFG_PATH% and %CORE_PEER_LOCALMSPID%.
PowerShell example:
Create a file named envVars.ps1, for instance, and place the following lines in it:
$env:FABRIC_CFG_PATH='C:\path\to\some directory\config'
$env:CORE_PEER_LOCALMSPID=42
Then, from a PowerShell session / another PowerShell script, call the file as follows to make the environment variable-definitions take effect for the current process (assuming the file is in the current directory):
./envVars.ps1
You will then able to refer to the newly defined variables as $env:FABRIC_CFG_PATH and $env:CORE_PEER_LOCALMSPID.
[1] See this answer.
After some study on the executables/batch files in windows, I have come to the conclusion that I need to write a batch .bat file to use the set command to set the env variables as I desire.
I have this PowerShell command line which usually opens a webpage via Google Chrome, but I would like that to open randomly with Chrome and Firefox.
start-process -FilePath chrome.exe "www.quora.com"
One PowerShell solution for usage in a PowerShell script or in PowerShell console window is as posted by Santiago Squarzon:
start-process -FilePath ('chrome.exe','firefox.exe'|get-random) "https://www.quora.com"
For understanding the two used PowerShell cmdlets and how they work, open a PowerShell console window, execute there the following commands, and read entirely the two help pages displayed for the two cmdlets very carefully.
help start-process
help get-random
One batch file solution is:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set /A "Browser=%RANDOM% & 1"
if %Browser% == 0 (set "Browser=chrome") else set "Browser=firefox"
start "" %Browser%.exe "https://www.quora.com"
endlocal
The first two command lines just define the required execution environment completely and creates a new environment variables list as copy of the current environment variables list and the last line just results in restoring the initial execution environment and environment variables list. The second and the last command line could be omitted if it is no problem to depend on Windows defaults or what the process calling this batch file defines as execution environment.
The third line uses an arithmetic expression to apply on a random number a bitwise AND with 1 to get assigned to the environment variable Browser randomly either 0 or 1.
The third line could be also:
set /A Browser=%RANDOM% %% 2
This arithmetic expression divides a random number by two and gets the remainder 0 or 1 assigned to the environment variable Browser.
The fourth line uses an IF condition to redefine the environment variable Browser on having currently the value 0 with the string chrome and otherwise with the string firefox.
The fifth line uses command start to start either chrome.exe or firefox.exe as separate process on which cmd.exe does not wait for self-termination before continuation of processing of the batch file with passing the URL to started executable. "" defines an empty string as optional title for the console window not opened at all as the two browsers are Windows applications with a graphic user interface.
For understanding the used Windows Commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
help echo or echo /?
help endlocal or endlocal /?
help if or if /?
help set or set /?
help setlocal or setlocal /?
help start or start /?
See also:
An A-Z Index of Windows CMD commands
An A-Z Index of Windows PowerShell commands
Windows CMD Shell How-to guides and examples
Windows PowerShell How-to guides and examples
Where is "START" searching for executables?
powershell.exe does the same. PowerShell and CMD use the Windows kernel library function CreateProcess to start the Chrome or Firefox process.
I suspect there is no good solution, but perhaps I'm overlooking something:
What I'm after is a way to:
(a) call a batch file from PowerShell in a way that robustly reflects its - implicit or explicit - exit code in PowerShell's automatic $LASTEXITCODE variable.
Notably, calling a batch file that exits with, say, whoami -nosuch || exit /b, should result in $LASTEXITCODE reflecting whoami's exit code, i.e. 1. This is not the case when you invoke a batch file (by name or path) from PowerShell: the exit code is 0 (by contrast, inside a cmd.exe session %ERRORLEVEL% is set to 1).
Also note that the invocation should remain integrated with PowerShell's output streams, so I am not looking for solutions based on System.Diagnostics.Process.
Furthermore, I have no knowledge of or control over the batch files getting invoked - I'm looking for a generic solution.
(b) without double-quoted arguments passed to the batch file getting altered in any way, and without cmd.exe's behavior getting modified in any way; notably:
^ characters should not be doubled (see below).
Enabling delayed expansion with /V:ON is not an option.
The only way I know how to solve (a) is to invoke the batch file via cmd /c call.
Unfortunately, this violates requirement (b), because the use of call seemingly invariably doubles ^ characters in arguments. (And, conversely, not using call then doesn't report the exit code reliably).
Is there a way to satisfy both requirements?
Note that PowerShell is only the messenger here: The problem lies with cmd.exe, and anyone calling a batch file from outside a cmd.exe session is faced with the same problem.
Example (PowerShell code):
# Create a (temporary) batch file that echoes its arguments,
# provokes an error, and exits with `exit /b` *without an explicit argument*.
'#echo off & echo [%*] & whoami -nosuch 2>NUL || exit /b' | Set-Content test.cmd
# Invoke the batch file and report the exit code.
.\test.cmd "a ^ 2"; $LASTEXITCODE
The output should be:
["a ^ 2"]
1
However, in reality the exit code is not reported:
["a ^ 2"]
0 # !! BROKEN
If I call with cmd /c call .\test.cmd instead, the exit code is correct, but the ^ characters are doubled:
PS> cmd /c call .\test.cmd "a ^ 2"; $LASTEXITCODE
["a ^^ 2"] # !! BROKEN
1 # OK
I've no idea why this works, but it does:
cmd /c '.\test.cmd "a ^ 2" & exit'
$LASTEXITCODE
Output:
["a ^ 2"]
1
Kudos to beatcracker for finding an effective workaround in his answer; let me add some background information and guidance:
First, to be clear, no workaround should be necessary; cmd.exe's behavior is clearly a bug.
cmd /c '.\test.cmd "a ^ 2" || exit' - i.e. || rather than & - is what one would expect to be an effective workaround too. The fact that only &, which unconditionally sequences commands, works, indicates that even cmd.exe-internally the failure status of the batch file isn't yet known as part of the same statement - only afterwards - which appears to be another manifestation of the bug.
Why an explicit exit call following the batch-file call as part of the same statement does relay the batch file's (zero or nonzero) exit code correctly is anyone's guess, but it seems to work.
Fortunately, the workaround is also effective for solving related exit-code problems in batch files that do not contain explicit exit /b / exit calls - see this answer.
Syntax considerations:
From PowerShell, the alternative to passing a single command-string is to pass individual arguments and escape the & character as `& (using `, the "backtick", PowerShell's escape character) so as to prevent PowerShell from interpreting it (quoting it as '&' would work too):
cmd /c .\test.cmd "a ^ 2" `& exit
From an environment that doesn't involve a shell, such as when launching from Task Scheduler, the `-escaping of & is not needed (and mustn't be used).
Not having to enclose the entire for-cmd.exe command in quotes makes it easier to pass arguments that (a) individually require double quotes and (b) involve references to PowerShell variables and/or expressions, given that the latter requires use of "..." rather than '...':
# Passing *individual* arguments makes double-quoting easier.
PS> cmd /c .\test.cmd "Version = $($PSVersionTable.PSVersion)" `& exit; $LASTEXITCODE
["Version = 7.2.0-preview.4"]
1
Using quoting of the entire for-cmd.exe command would be awkward in this case, due to the need to escape the argument-specific " chars.:
# Embedded double quotes must now be `-escaped.
PS> cmd /c ".\test.cmd `"Version = $($PSVersionTable.PSVersion)`" & exit"
["Version = 7.2.0-preview.4"]
1
The Native module (authored by me; install it from the PowerShell Gallery with Install-Module Native) comes with function ie, which:
automatically applies the above workaround.
generally compensates for problems arising from PowerShell's broken argument-passing to external programs (see this answer).
# After having run Install-Module Native:
# Use of function `ie` applies the workaround behind the scenes.
PS> ie .\test.cmd "Version = $($PSVersionTable.PSVersion)"; $LASTEXITCODE
["Version = 7.2.0-preview.4"]
1
The hope is that what function ie does will become a part of PowerShell itself, as part of the upcoming (in PowerShell v7.2) PSNativeCommandArgumentPassing experimental feature that is intended as an opt-in fix to the broken argument-passing - see GitHub issue #15143
In a Powershell script (.ps1)
Launched from the command line of a console (cmd.exe)
How can set and environment variable in the console,
so that when the Powershell script ends processing,
and exits to the console where was invoked...
the environment variable exists,
and can be read by a batch file,
or viewed with the SET command ?
do not want to set a 'Machine' or a 'User' variable...
only a console process variable...
the same variable you get if you use SET in the console
To run a PowerShell script from cmd.exe invariably requires a (powershell.exe / pwsh.exe) child process, and child processes fundamentally cannot set environment variables for their parent process[1].
Your best bet is to have your *.ps1 file output the name and value of the desired environment variable and then have the calling cmd.exe process create it, based on that output.
Security note: Blindly defining environment variables based on the name-value pairs output by another command (a *.ps1 script, in your case) should only be done if you trust that command not to output malicious definitions.
Here's a simple example (run directly from an interactive cmd.exe session):
for /f "delims== tokens=1,*" %v in ('powershell.exe -c "'FOO=bar'"') do #set "%v=%w"
The above defines environment variable %FOO% with value bar, based on the PowerShell command outputting the literal name-value pair FOO=bar.
Verify with echo %FOO%.
To extend this approach to defining multiple environment variables, make the command output each definition on its own line (which in PowerShell you can achieve by outputting an array of strings):
for /f "delims== tokens=1,*" %v in ('powershell.exe -c "'FOO=bar', 'BAZ=bam'"') do #set "%v=%w"
The above additionally defines %BAZ% with value bam.
To make this more convenient, I suggest creating a wrapper batch file (*.cmd) that performs the above:
Note that you'll have to use %%v and %%w instead of %v and %w there.
Instead of -c (for -Command) with the demo command, use -File with the path to your *.ps1 file to invoke it.
Also consider use of -NoProfile as well, to bypass loading of your PowerShell environment's $PROFILE file, which not only slows things down, but may pollute your command's output.
[1] As LotPings points out, child processes inherit copies of the parent process' environment variables. Modifications of these copies are never seen by the parent. A child process is fundamentally unable to modify its parent's environment, which is a restriction at the OS level - for good reasons: Modifying a running process' environment by an arbitrary (child) process would be a serious security concern.