How do you set variables NORMALLY after using setlocal? - batch-file

I'm trying to make a simple batch file ("javapath.bat") to add the Java compiler to the path when I need it so it won't be on the path all the time. I also want be able to do something like #call javapath.bat in other build scripts so the path can be added automatically when needed. Since those will be run repeatedly during the edit-save-compile-run grind, that means that javapath.bat needs to check if Java is already on the path and not readd it if it is, because apparently Microsoft thinks it's a good idea to let the path variable have lots of silly duplicates.
So to detect if it needs to be added I use setlocal to enable "command extensions" so I can use the environment variable string substitution thing. That ugliness works fine.
Then I use endlocal so I can actually set the enviroment variables without the changes being reverted at the end of the script. That's not working. Or, it certainly stops the variable changes being reverted, but it's not normal: it completely stops them from being visible locally, but they are still visible afterwards.
#echo off
setlocal enableextensions
if "%path:jdk1=%"=="%path%" (
endlocal
set ANT_HOME=C:\Program Files\Java\ant
set JAVA_HOME=C:\Program Files\Java\jdk1.7.0_07
path %ANT_HOME%\bin;%path%
path %JAVA_HOME%\bin;%path%
)
After the above, ANT_HOME and JAVA_HOME are properly set. But the only change to PATH is that "\bin;" has been prepended to it, because none of the variables set during the script seem to be visible until afterwards (so ANT_HOME and JAVA_HOME are blank, and the first change to PATH is forgotten). Therefore, running it twice adds Java to the path okay, and not Ant. I could hardcode the paths twice but this behavior is so bizarre and ridiculous and I've been stuck on it for an hour.
Edit: Adding enabledelayedexpansion had no effect either.

#echo OFF
ECHO starting %PATH%
if "%path:jdk1=%"=="%path%" CALL :addjava
ECHO.
ECHO resulting %PATH%
GOTO :eof
:addjava
set ANT_HOME=C:\Program Files\Java\ant
set JAVA_HOME=C:\Program Files\Java\jdk1.7.0_07
SET "path=%ANT_HOME%\bin;%JAVA_HOME%\bin;%path%"
GOTO :eof
This is what I'd use - other methods run afoul of the mininterpreted closing-parenthesis problem.
The key to understanding this odd behaviour is history. Batch has always substituted the parse-time value of any %var% into the code, then validated the result and executed if valid. As the language developed, it was necessary to maintain compatibility with existing batches, so you could only ADD new keywords and functionality, not remove or alter functionality.
So, as the capacity to call internal subroutines was added, and cascade instructions on a single line with '&' and allow multi-line instructions for if and for by enclosing the instructions in parentheses were introduced, and the capacity to use spaces and other separator characters in file or directory names was required, the batch language began to have a few little quirks.
It was a really bizarre decision to have the ! to access the run-time value of a variable invoked as a subclause of setlocal - personally, I'd have used a switch like ECHO on/off (ie EXPANSION on/off) but I'm not running the project. In the same way, DATE could have been equipped with a /u switch to return the date in a universal form, but the opportunity was missed (and continues to be missed, 17 years after NT4 and 5 wingenerations later...)

As others have noted, extensions should already be enabled except under rather extraordinary circumstances. All you need is to eliminate your SETLOCAL and restructure your IF a bit so that it exits the script if the PATH is already set.
#echo off
if not "%path:jdk1=%"=="%path%" exit /b
set "ANT_HOME=C:\Program Files\Java\ant"
set "JAVA_HOME=C:\Program Files\Java\jdk1.7.0_07"
path %ANT_HOME%\bin;%path%
path %JAVA_HOME%\bin;%path%
If you really need to enable extensions, then
#echo off
setlocal enableExtensions
if not "%path:jdk1=%"=="%path%" exit /b
endlocal
set "ANT_HOME=C:\Program Files\Java\ant"
set "JAVA_HOME=C:\Program Files\Java\jdk1.7.0_07"
path %ANT_HOME%\bin;%path%
path %JAVA_HOME%\bin;%path%
If your script has additional work to do, then
#echo off
setlocal enableExtensions
if not "%path:jdk1=%"=="%path%" goto :skip
endlocal
set "ANT_HOME=C:\Program Files\Java\ant"
set "JAVA_HOME=C:\Program Files\Java\jdk1.7.0_07"
path %ANT_HOME%\bin;%path%
path %JAVA_HOME%\bin;%path%
:skip
REM carry on with additional code as needed

Everything inside the if block is evaluated in one go. So %ANT_HOME% has no effect after set ANT_HOME, you want delayed expansion you need to type:
setlocal enabledelayedexpansion
if "%path:jdk1=%"=="%path%" (
set ANT_HOME=C:\Program Files\Java\ant
set JAVA_HOME=C:\Program Files\Java\jdk1.7.0_07
path = !ANT_HOME!;!path!
path = !JAVA_HOME!;!path!
)
:: important trick since they evaluate together %path% is still
:: what is inside local
endlocal & path %path%
path
Otherwise no delayed expansion. Also you need to use on undeleyed call with endlocal to escape the block. remember % variables never delay.

Related

set PATH with multiple lines

In a Batch file I need to add some paths to the PATH env variable. Since its a larger numer of long paths I tried to spread them on multiple line and tried to make the bat-file as clean as I can by indenting the lines.
But it seems that the spaces at the beginning of the newline (and so in the %PATH%) are interpreted as part of the actual path.
So
SET PATH=^
\\somewhere\Tools\strawberryperl\perl\site\bin;^
\\somewhere\Tools\strawberryperl\perl\bin;^
\\somewhere\Tools\strawberryperl\c\bin;^
\\somewhere\Tools\KDiff3;^
%PATH%
does not work (programs are not found). Is there some trick I can use?
Because it is a medium complex batch file some indentation would be nice.
for %%x in (
"\\somewhere\Tools\strawberryperl\perl\site\bin;"
"\\somewhere\Tools\strawberryperl\perl\bin;"
"\\somewhere\Tools\strawberryperl\c\bin;"
"\\somewhere\Tools\KDiff3;"
) do call set "path=%%path%%%%~x"
this will append the extra items to the path. You'd need to initialise path to nothing first if all you want is to build the directory sequence specified.
There is no way to have PATH ignore the leading spaces. I see two possible options if you want indented lines:
Option 1 - Use undefined variable to indent, so spaces never get in the value
#echo off
SET PATH=^
% =%\\somewhere\Tools\strawberryperl\perl\site\bin;^
% =%\\somewhere\Tools\strawberryperl\perl\bin;^
% =%\\somewhere\Tools\strawberryperl\c\bin;^
% =%\\somewhere\Tools\KDiff3;^
% =%%PATH%
Option 2 - Remove the spaces afterwards
#echo off
SET PATH=^
\\somewhere\Tools\strawberryperl\perl\site\bin;^
\\somewhere\Tools\strawberryperl\perl\bin;^
\\somewhere\Tools\strawberryperl\c\bin;^
\\somewhere\Tools\KDiff3;^
%PATH%
set "PATH=%PATH:; =%"
First let me start by informing you that adding to the PATH variable in this way is ONLY for the running session. Once the cmd session is closed that variable returns to its previous value.
Here are a suggestion, append each addition one by one:
SET "ToAdd=\\somewhere\Tools\strawberryperl\perl\site\bin;"
SET "ToAdd=%ToAdd%;\\somewhere\Tools\strawberryperl\perl\bin;"
SET "ToAdd=%ToAdd%;\\somewhere\Tools\strawberryperl\c\bin;"
SET "ToAdd=%ToAdd%;\\somewhere\Tools\KDiff3"
SET "PATH=%PATH%;%ToAdd%"
BTW, if you were hoping to add to the environment variable beyond the running session then it is important that you ignore anyone suggesting you use SETX instead of SET. (the variable will be truncated at 1024 bytes therefore corrupting it). Your best solutions would involve editing the registry and possibly using a built in tool such as powershell.
Edit
This shows the method mentioned in my comment and uses the same structure as Magoo's answer:
C:\MyDir\Paths.txt
\\somewhere\Tools\strawberryperl\perl\site\bin
\\somewhere\Tools\strawberryperl\perl\bin
\\somewhere\Tools\strawberryperl\c\bin
\\somewhere\Tools\KDiff3
batch file
#Echo Off
SetLocal EnableDelayedExpansion
For /F "UseBackQDelims=" %%A In ("C:\MyDir\paths.txt") Do Set "Path=!Path!;%%~A"
Echo(%Path%
EndLocal
Timeout -1
This means that you only really need to include the for loop each time instead of adding each of the paths to it.
Not even remotely bulletproof, but the Magoo's answer reminded me this. Just because someone, somewhere, could find a better usage for this construct
#echo off
setlocal enableextensions disabledelayedexpansion
for /f "delims=" %%a in ('echo "%path:;=" "%"
"\\somewhere\Tools\strawberryperl\perl\site\bin"
"\\somewhere\Tools\strawberryperl\perl\bin"
"\\somewhere\Tools\strawberryperl\c\bin"
"\\somewhere\Tools\KDiff3"
""') do (set "path=%%~a") & call set "path=%%path:" "=;%%"
path

Is it possible to get a variable from a string in batch?

I've recently started to learn batch for the sake of writing batch sims for a game that I've been playing. I was wondering if its possible to somehow iterate through like named variables (since I can't seem to find anything about a list?). Also I'm not sure if I can put a label to call to as a variable passed.
Code Example:
:: Enemy Fortress level.
SET EFORTLVL=4
:: Don't mess with anything below here only the variables above.
:: Enemy Fortress that will be simmmed against. Note this batch sim is only built to run against one tower, as this is what you should be doing.
SET EFORTRESS1="Foreboding Archway-%EFORTLVL%"
SET EFORTRESS2="Illuminary Blockade-%EFORTLVL%"
SET EFORTRESS3="Tesla Coil-%EFORTLVL%"
SET EFORTRESS4="Minefield-%EFORTLVL%"
SET EFORTRESS5="Forcefield-%EFORTLVL%"
call :sim 1
:sim
SET /a "COUNTER=1"
SETLOCAL enabledelayedexpansion
SET times=!ITERATIONS%1!
ENDLOCAL & SET TIMES=%times%
:whilesim
SETLOCAL enabledelayedexpansion
SET fort=!EFORTRESS%COUNTER%!
ENDLOCAL & SET FORT=%fort%
tuo.exe %DECK0% %ENEMY% surge random -v efort %FORT% yfort %YFORTRESSES% climb %TIMES% >> %PATH%\WarDefClimbData%DECK0%.txt
SET /a "COUNTER=COUNTER+1"
if %COUNTER% leq 5 GOTO :whilesim else GOTO :eof
The result that I get for the line on the console:
RESOLVED:
What I want to do is get a value from a variable that holds a string name that relates to the variable in question. (Ex when the for loop passes 1 I want to get EFORTRESS1 value, 2 I want EFORTRESS2 value etc).
E:\Programs\Tyrant Unleashed Optimizer>tuo.exe oconzer "VetDecks" surge random -v efort EFORTRESS1 yfort "Inspiring Altar #2" climb ITERATIONS1 1>>"e:\Programs\Tyrant Unleashed Optimizer\BatchSimResults"WarDefClimbDataoconzer.txt
Error: unrecognized fortress EFORTRESS1
Now I understand why its saying the error, what I don't understand is why its not getting the value from the string that is contained in FORT.
RESOLVED
Getting an endless loop, where the iteration variable isn't updating.
:sim
SETLOCAL ENABLEDELAYEDEXPANSION
SET "FORT=!EFORTRESS%1!"
ENDLOCAL&SET "fort=%fort%"
SET TIMES=ITERATIONS%2
SET LABEL=%3
The issue is to get the contents of (the contents of a variable), often call "indirection".
This is probably the easiest way. It uses setlocal enabledelayedexpansion which places cmd in delayedexpansion mode, where !var! is evaluated after its contents.
The drawback is that it must be invoked using setlocal, which establishes a local environment. The loacl environment must eventually be closed (you cannot keep opening more) and at that time, all changes to the environment are discarded and it is restored to its state at the point of executing setlocal.
The endlocal&... uses a parsing trick to transfer the changes out of the setlocal/endlocal bracket.
As for the other questions - yes, you can goto a variable (and the contents of the variable need not have a leading colon). It is quite possible to use goto somewhere%1 for instance, and supply %1 as a parameter as you've done. The text somewhere would be simply prepended to the value %1.
BTW - it would appear that you are changing path. This is not a good idea. path is the variable that contains a ;-separated list of directories which are searched for an executable if that executable is invoked and not found in the current directory. Best left well alone. Same goes for tmp and temp (which point to a temporary directory) and date and time and random and cd (the current date, time, a random number and the current directory)

How can I URL-encode spaces in an NT batch file?

I have the misfortune of working with a program which requires all filenames passed into it to be valid URLs. (No, I don't know why.) Rather than having to drop to the command line and hand-craft file: URLs each time, I'm throwing together a batch file onto which I can simply drop files dragged from the Windows GUI.
A full, proper URL encoder is beyond my needs or interest (most of the characters the app chokes on aren't valid in Windows filenames, anyway), but the two cases I do need to solve are backslashes and spaces. Backslashes are easily handled by variable replacement syntax (SET URL=%URL:\=/%), but spaces are tricky — % is special for both URLs and batch files.
Neither type of batch escaping I'm familiar with (^%, %%) allows the variable replacement to behave as desired and I haven't had any success Googling a solution. Can any batch gurus help me out?
Here's what I have so far:
#ECHO OFF
SETLOCAL
SET URLPATH=file:/%~dp1
SET URLPATH=%URLPATH:\=/%
REM none of these work
REM SET URLPATH=%URLPATH: =%20%
REM SET URLPATH=%URLPATH: =%%20%
REM SET URLPATH=%URLPATH: =^%20%
REM works; I just need to figure out how to generate it
SET URLPATH=file:/C:/Documents%%20and%%20Settings/bblank/example.dat
stupidapp.exe %URLPATH%
ENDLOCAL
Side note - I believe you want %~f1 instead of %~dp1
You need to switch over to delayed expansion.
#echo off
setlocal enableDelayedExpansion
set "URLPATH=file:/%~f1"
set "URLPATH=!URLPATH:\=/!"
set "URLPATH=!URLPATH: =%%20!"
stupidapp.exe !URLPATH!
endlocal
A bit more work is required if any of your file names happen to contain the ! character because it will be corrupted when %1 is expanded if delayed expansion is enabled.
#echo off
setlocal disableDelayedExpansion
set "URLPATH=file:/%~f1"
setlocal enableDelayedExpansion
set "URLPATH=!URLPATH:\=/!"
set "URLPATH=!URLPATH: =%%20!"
stupidapp.exe !URLPATH!
endlocal
endlocal
dbenham's solution is almost certainly preferable (being rather easier to read), but for the sake of completeness, here is an alternative solution:
SET URLPATH=file:/%~dp1
SET URLPATH=%URLPATH:\=/%
REM for each space in the path, split the path into the portions before and after
REM that space, then join them with an escaped space
:ESCAPE_SPACE
SET TRAILING=%URLPATH:* =%
CALL SET URLPATH=%%URLPATH: %TRAILING%=%%
SET URLPATH=%URLPATH%%%20%TRAILING%
IF NOT "%URLPATH%"=="%URLPATH: =%" GOTO ESCAPE_SPACE
stupidapp.exe %URLPATH%

How do SETLOCAL and ENABLEDELAYEDEXPANSION work?

I notice in most scripts, the two are usually in the same line as so:
SETLOCAL ENABLEDELAYEDEXPANSION
Are the two in fact separate commands and can be written on separate lines?
Will setting ENABLEDELAYEDEXPANSION have an adverse effect on a script if it is set on the first lines of the script and not disabled until the end of the script?
I think you should understand what delayed expansion is. The existing answers don't explain it (sufficiently) IMHO.
Typing SET /? explains the thing reasonably well:
Delayed environment variable expansion is useful for getting around
the limitations of the current expansion which happens when a line of
text is read, not when it is executed. The following example
demonstrates the problem with immediate variable expansion:
set VAR=before
if "%VAR%" == "before" (
set VAR=after
if "%VAR%" == "after" #echo If you see this, it worked
)
would never display the message, since the %VAR% in BOTH IF statements
is substituted when the first IF statement is read, since it logically
includes the body of the IF, which is a compound statement. So the IF
inside the compound statement is really comparing "before" with
"after" which will never be equal. Similarly, the following example
will not work as expected:
set LIST=
for %i in (*) do set LIST=%LIST% %i
echo %LIST%
in that it will NOT build up a list of files in the current directory,
but instead will just set the LIST variable to the last file found.
Again, this is because the %LIST% is expanded just once when the FOR
statement is read, and at that time the LIST variable is empty. So the
actual FOR loop we are executing is:
for %i in (*) do set LIST= %i
which just keeps setting LIST to the last file found.
Delayed environment variable expansion allows you to use a different
character (the exclamation mark) to expand environment variables at
execution time. If delayed variable expansion is enabled, the above
examples could be written as follows to work as intended:
set VAR=before
if "%VAR%" == "before" (
set VAR=after
if "!VAR!" == "after" #echo If you see this, it worked
)
set LIST=
for %i in (*) do set LIST=!LIST! %i
echo %LIST%
Another example is this batch file:
#echo off
setlocal enabledelayedexpansion
set b=z1
for %%a in (x1 y1) do (
set b=%%a
echo !b:1=2!
)
This prints x2 and y2: every 1 gets replaced by a 2.
Without setlocal enabledelayedexpansion, exclamation marks are just that, so it will echo !b:1=2! twice.
Because normal environment variables are expanded when a (block) statement is read, expanding %b:1=2% uses the value b has before the loop: z2 (but y2 when not set).
ENABLEDELAYEDEXPANSION is a parameter passed to the SETLOCAL command (look at setlocal /?)
Its effect lives for the duration of the script, or an ENDLOCAL:
When the end of a batch script is reached, an implied ENDLOCAL is
executed for any outstanding SETLOCAL commands issued by that batch
script.
In particular, this means that if you use SETLOCAL ENABLEDELAYEDEXPANSION in a script, any environment variable changes are lost at the end of it unless you take special measures.
The ENABLEDELAYEDEXPANSION part is REQUIRED in certain programs that use delayed expansion, that is, that takes the value of variables that were modified inside IF or FOR commands by enclosing their names in exclamation-marks.
If you enable this expansion in a script that does not require it, the script behaves different only if it contains names enclosed in exclamation-marks !LIKE! !THESE!. Usually the name is just erased, but if a variable with the same name exist by chance, then the result is unpredictable and depends on the value of such variable and the place where it appears.
The SETLOCAL part is REQUIRED in just a few specialized (recursive) programs, but is commonly used when you want to be sure to not modify any existent variable with the same name by chance or if you want to automatically delete all the variables used in your program. However, because there is not a separate command to enable the delayed expansion, programs that require this must also include the SETLOCAL part.
A real problem often exists because any variables set inside will not be exported when that batch file finishes. So its not possible to export, which caused us issues. As a result, I just set the registry to ALWAYS used delayed expansion (I don't know why it's not the default, could be speed or legacy compatibility issue.)

How to Use an Environment Variable as an Environment Variable Name

In my pursuit of a solution to another environment-variable/batch-file related problem, I have once again come across a problem I have visited before (but cannot for the life of me remember how, or even if I solved it).
Say you have two BAT files (or one batch file and the command line). How can one pass an environment variable name to the other so that it can read the variable? The following example does not work:
A.BAT:
#call b.bat path
B.BAT:
#echo %%1%
> A.BAT
> %1
> B.BAT path
> %1
It is easy enough to pass the environment variable name, but the callee cannot seem to use it. (I don’t remember if or how I dealt with this the last time it came up, but I suspect it required the less-than-ideal use of redirecting temporary BAT files and calling them and such.)
Any ideas? Thanks.
You can use a little trick which unfortunately is nowhere documented:
call echo %%%1%%
Then you can use delayed expansion:
setlocal enabledelayedexpansion
echo !%1!
Delayed expansion helps here mostly because it uses other delimiters for the variable and evaluates them directly prior to running the command, while normally the evaluation might clash with normal parameter expansion.
Another way of overdoing this would be a subroutine:
call :meh "echo %%%1%%"
...
:meh
%~1
goto :eof
All examples, including the other answer, have one thing in common here: They all force cmd to evaluate variables/parameters twice. It won't work otherwise, since the first evaluation must produce %VariableName%, while the second will expand that to the variable's contents.
You can find the code also on my SVN.
B.BAT:
FOR /F "delims=" %%a IN ('echo.%%%1%%') DO set inputvar=%%a
echo %inputvar%
That is one way of doing it.
If all you want to do is echo it, you can do: echo.%%%1%%|more or echo %%%1%%|find /v ""

Resources