Unable to append to environment variable in batch file - windows

I'm trying to append four directories to %pythonpath%.
The directories are:
C:\src\tensorflow\models\research
C:\src\tensorflow\models\research\object_detection
..\utils
..\mail
When user is User42, %pythonpath% is always set to:
;C:\src\tensorflow\models\research\object_detection;..\utils;..\mail
Why is the first path ignored/overwritten?
#echo off
if "%username%"=="User42" (
set pythonpath=%pythonpath%;C:\src\tensorflow\models\research
set pythonpath=%pythonpath%;C:\src\tensorflow\models\research\object_detection
) else (
:: Other path
)
:: This is common to all users
set pythonpath=%pythonpath%;..\utils;..\mail
echo %pythonpath%

Windows command processor replaces all environment variable references with syntax %VariableName% in a command block starting with ( and ending with matching ) during parsing phase of the next command line to execute on which a command block begins. In this case this means all %pythonpath% in both branches of the IF condition are substituted already by the current value of environment variable pythonpath before the IF condition is executed at all. This behavior can be seen by running the batch file without #echo off from within a command prompt window as in this case Windows command processor outputs the command lines after being parsed before execution.
The solution is using delayed expansion as also explained by help of command SET output on running in a command prompt window set /? on an IF and a FOR example, or avoiding the definition or modification of an environment variable and referencing it once again in same command block.
Here is a solution which works without usage of delayed expansion:
#echo off
set "Separator="
if defined pythonpath if not "%pythonpath:~-1%" == ";" set "Separator=;"
if /I "%username%" == "User42" (
set "pythonpath=%pythonpath%%Separator%C:\src\tensorflow\models\research;C:\src\tensorflow\models\research\object_detection"
) else (
rem Other path is added here to environment variable pythonpath.
)
rem This is common to all users. Variable pythonpath is defined definitely now.
for %%I in ("%CD%") do set "ParentPath=%%~dpI"
set "pythonpath=%pythonpath%;%ParentPath%utils;%ParentPath%mail"
echo %pythonpath%
It additionally makes sure there is not ;; in value of pythonpath in case of there is already a semicolon at end. And it makes sure pythonpath is not defined with a ; at beginning if this environment variable does not exist at all before first IF condition.
Further there are no relative paths added to pythonpath because of determining absolute path for ..\utils and ..\mails before appending them to pythonpath.
For understanding the used 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.
echo /?
if /?
rem /?
set /?
BTW: Invalid labels :: as comments should not be used in command blocks. That can result in undefined behavior on execution. It is safer to use command REM for comments.

%pythonpath% used twice between parentheses will be evaluated on read before execution begins.
This is why %pythonpath% has the same value on the 2nd set.
You can use call set to force evaluation on the variable with doubled %s.
This has a similar effect to enabledelayedexpansion seen in setlocal /?
and if /?.
#echo off
if "%username%"=="User42" (
set "pythonpath=%pythonpath%;C:\src\tensorflow\models\research"
call set "pythonpath=%%pythonpath%%;C:\src\tensorflow\models\research\object_detection"
) else (
:: Other path
)
:: This is common to all users
set "pythonpath=%pythonpath%;..\utils;..\mail"
echo %pythonpath%

Related

Why does my batch script not interpret a string correctly

In my second if statement, I want to filter out "tool" or "tool.bat" from the final list of filenames. However, the final list of filenames includes "tool" and total_bags is being incremented. I was wondering what I did incorrectly that's causing the program to not catch this case.
set /A total_bags=0
set target=%~1
if "%target%"=="" set target=%cd%
set LF=^
rem Previous two lines deliberately left blank for LF to work.
for /f "tokens=1 delims=. " %%i in ('dir /b /s /a:-d "%target%"') do (
set current_file=%%~ni
echo !unique_files! | find "!current_file!:" > nul
if NOT !ERRORLEVEL! == 0 (
if NOT !current_file! == "tool.bat" (
set /A total_bags=total_bags+1
set unique_files=!unique_files!!current_file!:
)
)
)
echo %unique_files::=!LF!%
echo %total_bags%
endlocal
The condition if NOT "%current_file%" == "tool.bat" as initially used does not work because of %current_file% is replaced already by current string of the environment variable current_file respectively an empty string on Windows command processor is processing the entire command block starting with ( and ending with matching ) before executing command FOR. That can be seen on debugging the batch file. See also Variables are not behaving as expected for a very good and short example explaining how the Windows command interpreter (CMD.EXE) parses scripts.
It is in general not advisable to assign the string already assigned to a loop variable to an environment variable which is not further modified inside a FOR loop. It would be better to use %%~ni everywhere in your code on which the current file name needs to be referenced.
The usage of delayed expansion requires enabling it with setlocal EnableDelayedExpansion (or with setlocal EnableExtensions EnableDelayedExpansion to enable explicitly also the command extensions enabled by default) as it is not enabled by default in comparison to the command extensions. Then the Windows command processor parses each command line a second time and expands !current_file! on execution of command IF.
But even if NOT !current_file! == "tool.bat" evaluates always to true for the batch file with name tool.bat because of set current_file=%%~ni results in assigned to the environment variable current_file only the string tool (file name without file extension) and the left string is not enclosed in double quotes while the right string is always enclosed in double quotes. The command IF does not remove the double quotes from right string before comparing the two strings.
The batch file in question misses also set unique_files= above the FOR loop to undefine explicitly the environment variable unique_files in case of being already defined by chance on starting the batch file, for example from a previous execution within a command prompt window.
Another problem with the batch file in question is that maximum string length of variable name + equal sign + string assigned to the environment variable is 8191 characters which is a problem on several thousands of file names are concatenated to a long string assigned to one environment variable like unique_files.
I suggest to use this batch file with comments explaining it.
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem Delete all environment variables of which name starts very unusual
rem with a question mark existing already by chance (with exception of
rem those environment variables with multiple question marks in name).
for /F "delims=?" %%I in ('set ? 2^>nul') do set "?%%I?="
rem Search with the string passed as first argument or simply within current
rem directory recursively for all files and define for each file name an
rem environment variable with a question mark at beginning and one more at
rem end of the variable name. A file name cannot contain a question mark.
rem The value assigned to the environment variable does not matter. As it
rem is not possible to define multiple environment variables with same name
rem and environment variable names are case-insensitive, there is just one
rem environment variable defined on multiple files have same file name.
rem The batch file itself is ignored because of the IF condition.
for /F "delims=" %%I in ('dir "%~1" /A-D /B /S 2^>nul') do if not "%%I" == "%~f0" set "?%%~nI?=1"
rem Initialize the file counting environment variable.
set "FileCount=0"
rem Output all file names which are the environment variable names sorted
rem alphabetically with the question marks removed and additionally count
rem the number of file names output by this loop.
for /F "eol=| delims=?" %%I in ('set ? 2^>nul') do set /A "FileCount+=1" & echo %%I
rem Output finally the number of unique file names excluding file extensions.
echo %FileCount%
rem Restore initial execution environment which results also in the
rem deletion of all environment variables defined during batch execution.
endlocal
It does not use delayed expansion and for that reason works also for file names containing one or more ! in file name which would be processed wrong on enabling delayed expansion on line set current_file=%%~ni because of the exclamation mark(s) in file name would be interpreted as begin/end of a delayed expanded environment variable reference.
There is defined an environment variable for each unique file name. The number of environment variables is limited only by the total available memory for environment variables which is 64 MiB. That should be enough even for several thousands of unique file names in the directory tree.
For understanding the used 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.
call /? ... explains %~f0 which references full name of argument 0 which is the full qualified file name of the currently processed batch file and %~1 referencing first argument with perhaps existing surrounding " removed from argument string.
dir /?
echo /?
endlocal /?
for /?
if /?
rem /?
set /?
setlocal /?
Read the Microsoft documentation about Using command redirection operators for an explanation of 2>nul. The redirection operator > must be escaped with caret character ^ on the FOR command lines to be interpreted as literal character when Windows command interpreter processes this command line before executing command FOR which executes the embedded dir or set command line with using a separate command process started in background with %ComSpec% /c and the command line within ' appended as additional arguments.

Windows cmd shell: if-then-else weirdness for block statements

Trying to setup a simple build script that will expand the path based on other environment variables. This little script works fine:
echo off
call c:\vstudio\vc\bin\vcvars32.bat
set _ISGIT=1
echo current path is %PATH%
if defined _ISGIT set PATH=c:\git\bin;%PATH%
But if I want to do execute multiple lines based on the existence of the _ISGIT variable, then I thought this would work
echo off
call c:\vstudio\vc\bin\vcvars32.bat
set _ISGIT=1
echo current path is %PATH%
if defined _ISGIT (
set PATH=c:\git\bin;%PATH%
set PATH=c:\foo;%PATH%
)
But that yields the following output:
D:\>test.cmd
D:\>echo off
current path is C:\vstudio\Common7\IDE\CommonExtensions\Microsoft\TestWindow;C:\Program Files (x86)\MSBuild\14.0\bin;C:\
vstudio\Common7\IDE\;C:\vstudio\VC\BIN;C:\vstudio\Common7\Tools;C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319;C:\vstudio
\VC\VCPackages;C:\Program Files (x86)\HTML Help Workshop;C:\vstudio\Team Tools\Performance Tools;C:\Program Files (x86)\
Windows Kits\10\bin\x86;C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\;C:\ProgramData\Oracl
e\Java\javapath;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\U
sers\jselbie\.dnx\bin;C:\Program Files\Microsoft DNX\Dnvm\;C:\Program Files (x86)\Windows Kits\10\Windows Performance To
olkit\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\nodejs\;C:\Program Files (x86)\Skype\Phone\;C:\WINDOWS\s
ystem32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\NVIDIA Co
rporation\PhysX\Common;C:\Users\jselbie\.dnx\bin;C:\Users\jselbie\AppData\Roaming\npm;%USERPROFILE%\AppData\Local\Micros
oft\WindowsApps;
MSBuild\14.0\bin was unexpected at this time.
The MSBuild\14.0\bin was unexpected is likely a side effect of the original path containing a directory with a space. The presence of a space in the expanded command with the () seems to throw the script off.
How do I workaround this without having to have independent if defined statements?
There are two things wrong. First, one of the directories in the path contains parentheses. Variable expansion is performed before parsing, so the closing parenthesis from PATH is taken as the closing parenthesis of the IF block. To fix this, you need to put your assignment in quotes: set "PATH=...".
Secondly, inside a block (denoted by parentheses) all environment variables in the whole block are first expanded at once. Then the block is parsed. This means the expanded path in the second line is the same as it is on the first line. It is not changed by the first line. To fix this, you should either change the path entirely in one line:
if defined _ISGIT (
set "PATH=c:\foo;c:\git\bin;%PATH%"
)
or use delayed expansion:
setlocal enabledelayedexpansion
if defined _ISGIT (
set "PATH=c:\git\bin;!PATH!"
set "PATH=c:\foo;!PATH!"
)
Delayed expansion works by expanding a variable at a later stage, i.e. after the variable has been assigned by the first line. It has to be enabled with the setlocal command before it can be used.
set "PATH=c:\git\bin;%PATH%"
set "PATH=c:\foo;%PATH%"
using the quotes makes cmd interpret the quoted-string as a single token, so it doesn't see the ) within the variable [ie path] which is closing the if defined ... (
A contribution to nearly exhaustive Klitos Kyriacou's answer.
There is an implicit endlocal command at the end of a batch
file.
Hence, your test.cmd should be as follows (read ENDLOCAL):
#echo off
call c:\vstudio\vc\bin\vcvars32.bat
set _ISGIT=1
echo current path is %PATH%
setlocal enabledelayedexpansion
if defined _ISGIT (
set "PATH=c:\git\bin;!PATH!"
set "PATH=c:\foo;!PATH!"
)
endlocal&(
set "PATH=%PATH%"
)
or as follows (read CALL, note doubled percent signs):
#echo off
call c:\vstudio\vc\bin\vcvars32.bat
set _ISGIT=1
echo current path is %PATH%
if defined _ISGIT (
call set "PATH=c:\git\bin;%%PATH%%"
call set "PATH=c:\foo;%%PATH%%"
)

Extracting file name from array element in batch

I have an environment variable like this
set BINARY[0]=C:\binary.bin
From which I'm trying to extract the full file name
set "x=0"
:binloop
if defined BINARY[%x%] (
call echo %%BINARY[%x%]%%
FOR %%i IN ("%%BINARY[%x%]%%") DO (
set FNAME=%%~nxi
)
set /a "x+=1"
GOTO binloop
)
rem ...
However for some reason, it tries to do:
set FNAME=%BINARY[0]%
instead of
set FNAME=binary.bin
What's wrong with the code and why?
Open a command prompt window, run set /? and read the output help pages explaining when and how to use delayed expansion in a code block for the commands IF and FOR.
%% in a batch file is interpreted as literal percent character which is the reason why a loop variable in a command executed directly in a command prompt window must be specified with just one percent sign while the same loop in a batch file requires two percent signs on referencing the loop variable.
When the Windows command processor encounters an opening parenthesis which marks the beginning of a command block, it searches for the matching closing parenthesis and replaces all environment variables references with syntax %VariableName% by the current value of the variable or nothing in case of variable does not exist. Then after the entire command block was parsed the IF or FOR is executed and used is once or more times the already preprocessed command block.
You could use
#echo off
setlocal EnableExtensions EnableDelayedExpansion
set "BINARY[1]=C:\binary1.bin"
set "BINARY[0]=C:\binary0.bin"
set "x=0"
:binloop
if defined BINARY[%x%] (
call echo %%BINARY[%x%]%%
for %%i in ("!BINARY[%x%]!") do (
set FNAME=%%~nxi
set FNAME
)
set /a "x+=1"
goto binloop
)
endlocal
which outputs
C:\binary0.bin
FNAME=binary0.bin
C:\binary1.bin
FNAME=binary1.bin
The command line
call echo %%BINARY[%x%]%%
is something special. This line is preprocessed before execution of command IF to
call echo %BINARY[0]%
respectively on second run to
call echo %BINARY[1]%
By usage of command CALL the single command line is processed like a subroutine or another batch file which means the line is preprocessed once more resulting in execution of
echo C:\binary0.bin
and on second run in execution of
echo C:\binary1.bin
which is the reason why the output is as expected here. But there is no double preprocessing for the environment variable reference in FOR.
Much better would be most likely the following code:
#echo off
setlocal EnableExtensions EnableDelayedExpansion
set "BINARY[1]=C:\binary1.bin"
set "BINARY[0]=C:\binary0.bin"
for /F "tokens=1* delims==" %%I in ('set "BINARY[" 2^>nul') do (
set "FNAME=%%~nxJ"
set FNAME
)
endlocal
The command set outputs all variables with their name and equal sign and their values which start with the specified string when there is whether parameter /A or /P used and the parameter does not contain an equal sign in an alphabetically sorted list. So the output of
set "BINARY[" 2>nul
as used in the command FOR is
BINARY[0]=C:\binary0.bin
BINARY[1]=C:\binary1.bin
which is processed by the FOR loop which splits each line into two strings based on first occurrence of the equal sign because of tokens=1* delims==. The first string is the variable name assigned to loop variable I. And the second string is everything after first equal sign assigned to loop variable J being the next character in ASCII table.
2>nul is used to suppress the error message output by command SET to STDERR by redirecting it to device NUL if there is no environment variable defined with a name starting with BINARY[ in any case. The redirection operator > must be escaped with ^ as otherwise command processor would exit batch processing on this line because of 2>nul resulting in a syntax error on FOR command line at this position.
Note: Because of alphabetically sorted output by command SET the environment variable BINARY[10] is output after BINARY[0] and before BINARY[1] and BINARY[2]. So if the order is important, the first batch solution is needed or the environment variables are created with number in square brackets have all same number of digits with leading zeros, i.e. 00000, 00001, ..., 00002, 00010, 00011, ...
For understanding the used 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.
call /?
echo /?
endlocal /?
for /?
goto /?
if /?
set /?
setlocal /?
And see also Microsoft article about Using command redirection operators.

Windows command line tilde operators for full environment variables?

In cmd, you can use the tilde "operator" to do some cool tricks with arguments passed in. For example, %~dp0 returns the pathname of the current script.
Can you do that for any environment variable? For example:
set foo=1234.exe
echo %~nfoo%
Is there a way to accomplish this?
You can also filter your variable through a for loop instead of a subroutine:
setlocal
set foo=1234.exe
for %%I in ("%foo%") do echo %%~nI
Yes, sort of.
In the following file (I named test1.cmd) there is an example of passing a file path and name to a subroutine in the same .cmd file and getting the drive letter and path back. By setting more environment variables on the last line of the subroutine you could return more combinations of drive letter, path, file name, attributes, etc.
That last line is the important part. The Windows command processor evaluates a line at a time, so that line first expands environment variables to their text values and then proccesses the line. That expanded line destroys the scope of the subroutine, returning to the outer scope (endlocal), sets a new variable (mytest2) to the value of the subroutine's (expanded) %dp1, then executes a goto :eof, which returns to the calling line.
setlocal
set mytest=c:\windows\a file with spaces in name.txt
call :mytest2 "%mytest%"
echo %mytest2%
:ender
endlocal
goto :eof
:mytest2
setlocal
echo %~dp1
endlocal && set mytest2=%~dp1 && goto :eof

Concatenate file paths to an environment variable in batch script

I have made a bat script that should copy the list of folders to a variable but I don't get anything in the variable. In other words, when I echo the variable after my for loop I get the expected output, but in the shell outside after executing the script, I don't see anything set in my variable. How can I get all the variables to copy correctly?
I am using Windows 7.
Batch FIle (script.bat):
#echo off
setlocal enabledelayedexpansion enableextensions
for /r /D %%x in (*) do (
SET PATH_VALUE=%%x;!PATH_VALUE!
)
echo %PATH_VALUE%
Output of windows cmd utility
C:\test> script.bat
C:\test\1;C:\test\2
C:\test> echo %PATH_VALUE%
%PATH_VALUE%
How do I get the %PATH_VALUE% as an environment variable? I found a similar question here but it doesn't quite answer my case.
That is because of your SETLOCAL command that you use to enable delayed expansion. Yes it provides the delayed expansion you need, but it also localizes environment changes. As soon as your batch script ends, there is an implicit ENDLOCAL, and the old environment is restored.
You can pass the value across the ENDLOCAL barrier by adding the following to the end of your script:
endlocal&set "PATH_VALUE=%PATH_VALUE%"
or you could write it like:
(
endlocal
set "PATH_VALUE=%PATH_VALUE%"
)
Both of the above work because the blocks of code are expanded and parsed prior to the ENDLOCAL executing, but the SET statement with the expanded value is executed after the ENDLOCAL.

Resources