I have a batch file with the following header:
#Echo off
SETLOCAL EnableDelayedExpansion EnableExtensions
Within a If statement, enclosed in parenthesis (), I have the following code:
Echo SOME_VAR|FindStr /r /C:"^.*SOME.*$"
Echo Error: !errorlevel!
Echo %%SOME_VAR%%|FindStr /r /C:"^.*SOME.*$"
Echo Error: !errorlevel!
This will print:
SOME_VAR
Error: 0
Error: 1
If SOME_VAR is an existing environment variable, this is what I get. If I delete the variable, I get the expected success.
What happens here? Do I need to escape something more? How can I get a successful find on the 2nd one if the variable exists? I'm only interested in doing a text search where the searched text contains the % character and happens to match an existing variable name.
By the way, the source for the comparison will eventually be a variable too, in which I've loaded the PATH as read from the Windows Registry. So eventually, the string I am searching for will become /C:"^.%%SOME_VAR%%;.*$"
My PATH variable looks like this:
%SOME_VAR%;C:\Windows\system32...................etc
Yes, there is another layer of escape required because each side of the pipe is executed via CMD /C with a command line context, not a batch context. The initial batch parser transforms %%SOME_VAR%% to %SOME_VAR%. The command line parser then leaves %SOME_VAR% as is if the variable is undefined. We need to prevent the expansion if the variable is defined. Doubling the percents does not work in a command line context. The solution is to insert a disappearing caret somewhere between the percents, like %^SOME_VAR%. The caret is treated as part of the variable name, so it prevents expansion of the variable (unless you have a variable named ^SOME_VAR). After failed expansion, the caret is consumed by the normal escape process. The caret must be escaped so that the batch parser passes the caret to the CMD /C command line context.
The final batch line becomes:
Echo %%^^SOME_VAR%% | FindStr SOME_VAR
Note: I simplified the FINDSTR command into a much simpler, but equivalent search.
When you modify the search on the right to include the percents, you will need to insert the escaped caret as well.
Update in response to question in comment
The following code demonstrates some possible ways to work with variables:
#echo off
setlocal disableDelayedExpansion
:: Put both the text you want to search, as well as the search itself, in variables
set "text=%%SOME_VAR%%;C:\Windows\system32...................etc"
set "search=%%SOME_VAR%%"
echo text=%text%
echo search=%search%
echo(
echo(
echo Starting with delayed expansion disabled
echo -----------------------------------------
echo(
:: Use delayed expansion to safely expand any variable without worrying about content.
:: Both sides of the pipe are executed in a command line context with delayed expansion disabled.
:: You must use CMD /V:ON to enable delayed expansion within the pipe.
echo Test when SOME_VAR is undefined:
set "SOME_VAR="
if 1==1 (
cmd /v:on /c echo !text!|cmd /v:on /c findstr "!search!"
)
echo(
echo Test when SOME_VAR is defined:
set "SOME_VAR=DEFINED"
if 1==1 (
cmd /v:on /c echo !text!|cmd /v:on /c findstr "!search!"
)
echo(
setlocal enableDelayedExpansion
echo(
echo(
echo Now try with delayed expansion enabled
echo -----------------------------------------
echo(
:: Even though delayed expansion is enabled within the batch script, it is still disabled
:: within the pipe, so we still have to explicitly enable it via CMD /V:ON.
:: But now we must prevent expansion of the variables while in the batch context.
:: You have two options:
:: 1) Escape the !. The escape syntax changes depending on whether it is inside quotes or not:
echo Escape test:
if 1==1 (
cmd /v:on /c echo ^^!text^^!|cmd /v:on /c findstr "^!search^!"
)
echo(
:: 2) Enclose both sides of the pipe within parentheses (very weird, but it works)
echo Parentheses test:
if 1==1 (
(cmd /v:on /c echo !text!)|(cmd /v:on /c findstr "!search!")
)
--OUTPUT--
text=%SOME_VAR%;C:\Windows\system32...................etc
search=%SOME_VAR%
Starting with delayed expansion disabled
-----------------------------------------
Test when SOME_VAR is undefined:
%SOME_VAR%;C:\Windows\system32...................etc
Test when SOME_VAR is defined:
%SOME_VAR%;C:\Windows\system32...................etc
Now try with delayed expansion enabled
-----------------------------------------
Escape test:
%SOME_VAR%;C:\Windows\system32...................etc
Parentheses test:
%SOME_VAR%;C:\Windows\system32...................etc
Related
This part works well until there is an ampersand in the filename, in which case it crashes my script completely.
echo %filename% | findstr /i /b /c:"%name% (%year%)"
I can't just put the filename into quotation marks because I need to find the string at the beginning. So how can I do both?
For command line use:
echo %file^name:^&=^^^&% | ...
Inside a batch file
echo %%filename:^&=^^^&%% | ...
How it works?
As a pipe creates two new cmd.exe instances, the echo ... will be parsed twice.
The trick is to expand the filename only in the second expansion.
And then to expand and replace the & with ^& to avoid problems with the &.
The caret will be used to escape the ampersand and itself will be removed.
In the second expansion the parser only sees echo %filename:&=^&%.
To force the expansion into the second parse step, the percent signs have to be doubled for batch files.
From the command line, this doesn't work, but a simple caret anywhere in the variable name works.
Alternative solution:
echo "%filename%" | findstr /i /b /c:^"\"%filename% (%year%)\""
This adds simply quotes and uses also quotes in the search expression
Another option is to use delayed expansion, which requires an explicit cmd with the /v:on option.
cmd /v:on /c "(echo !filename!)" | findstr /i /b /c:"%name% (%year%)"
If your batch script already has enabled delayed expansion, then parentheses around the left side are needed to prevent the delayed expansion from occurring within the parent script (see Why does delayed expansion fail when inside a piped block of code?). The child process will still default to disabled delayed expansion, so the cmd /v:on /c ... is still needed.
#echo off
setlocal enableDelayedExpansion
...
(cmd /v:on /c "(echo !filename!)") | findstr /i /b /c:"%name% (%year%)"
Another way to delay the expansion until the sub-process is to escape the expansion
#echo off
setlocal enableDelayedExpansion
...
cmd /v:on /c "(echo ^!filename^!)" | findstr /i /b /c:"%name% (%year%)"
I want to execute some program passing an argument. The argument changes depending of the day of week and it is an url
Code:
#echo off
setlocal enableDelayedExpansion
for /f %%a in ('wmic path win32_localtime get dayofweek /format:list ^| findstr "="') do (set %%a)
if %dayofweek% == 7(
EXIT
)
set link="http://backlog.telecom.pt/maps/selector/download?map_name=workline_fornecedores&organization_id=1"
if %dayofweek%==5 (
set link="http://backlog.telecom.pt/maps/selector/download?map_name=all_fornecedores&organization_id=1"
)
REM start /wait D:\Planview\Services\BackLog_Integration_Client_Service\Backlog_Integration_Client_Exe\Backlog_Integration_Client_Exe.exe %link%
REM timeout /t 600 /nobreak > NUL
REM start D:\Planview\Services\PV_Backlog_ProcessData_Service\PV_Backlof_ProcessData_Exe\PV_Backlog_ProcessData_Exe.exe
I read that ^ before & would work to escape the & char, but for me it never did and the only way i managed to do it was enableDelayedExpansion and encapsulate the url in ", but this brought me a problem.. my variable instead of having the url has "url".
I tried to do set link=%link:"% but it did not worked.
I'll try to give you some advice via simple examples:
#setlocal enableDelayedExpansion
rem This fails because & is a "poison" character (an instruction or operator)
echo abc&xyz
rem This succeeds because & is escaped
echo abc^&xyz
rem This succeeds because & is quoted
echo "abc&xyz"
rem This succeeds, but it consumes the escape: stored value = abc&xyz
set test=abc^&xyz
rem So this fails because & is not escaped
echo %test%
rem You could "fix" above by double escaping when doing SET so stored value = abc^&xyz
rem But I don't like this - I pretty much never do it
set test=abc^^^&xyz
rem Now this works
echo %test%
rem Better to single escape and use delayed expansion.
set test=abc^&xyz
rem This works because poison characters don't bother delayed expansion
echo !test!
rem You could use quotes instead of escape, but now quotes are in value
set test="abc&xyz"
rem Delayed expansion is not needed because value is quoted
echo %test%
rem Putting the leading quote before the variable still quotes everything
rem But now the quotes are not in the value, as desired. Value = abc&xyz
set "test=abc&xyz"
rem Now you must use delayed expansion because value is not quoted
echo !test!
So the general rules of thumb that I like to use when poison characters are involved:
Use quotes around the entire assignment when SETting a variable: set "var=value"
Use delayed expansion when expanding a variable: !var!
Wouldn't it be nice if those rules solved everything. But of course batch is not that simple. There are situations where these simple rules will fail, but it should be enough to get you started.
I just found out that if you do the following:
set Variable=Test & echo %Variable% --Outputs "%Variable%"
echo %Variable% --Outputs "Test"
The change won't take effect until a new line runs. I need to have it take effect immediately as I need to use it with a very long, one-lined command.
You need delayed expansion or call echo:
#echo off
setlocal enableDelayedExpansion
set var=val&echo !var!
endlocal
set var=val&call echo %%var%%
If you have compositions of commands put together with & or in brackets the set command will take effect after all of them are executed.So you need or delayed expansion (which will allow you to access the variables with ! instead of %) or call
To enable the delayed expansion in command prompt you need to start like this cmd /v:on :
>cmd /v:on
>set Variable=Test & echo !Variable!
I am fighting with little piece of code for last two days.
In this I am not able to set variable in a for loop.
I want to assign a filename to a variable for string manipulation.
echo off
for /f %%a IN ('dir /b *_ah.ttf') DO (
set /a fName=%%~na
echo %fName%
)
When I echo fName variable I get only last filename repeatedly number of times for for loop count.
(I want to pass this variable as an argument to some batch file as follows
ttfhnt --strong-stem-width=D -i %%a %fName:~0,-3%.ttf
but its failing due to above problem)
Can somebody help me please?
When the cmd parser reads a line or a block of lines (the code inside the parenthesis), all variable reads are replaced with the value inside the variable before starting to execute the code. If the execution of the code in the block changes the value of the variable, this value can not be seen from inside the same block, as the read operation on the variable does not exist, as it was replaced with the value in the variable.
This same behaviour is seen in lines where several commands are concatenated with &. The line is fully parsed and then executed. If the first commands change the value of a variable, the later commands can not use this changed value because the read operation replace.
To solve it, you need to enable delayed expansion, and, where needed, change the syntax from %var% to !var!, indicating to the parser that the read operation needs to be delayed until the execution of the command.
And set /A is only used for arithmetic operations
setlocal enabledelayedexpansion
for /f "delims=" %%a IN ('dir /b *_ah.ttf') DO (
set "fName=%%~na"
echo "!fName!" "!fName:~0,-3!"
)
edited to adapt to comments
While for command is able to execute a command (in the OP code, the dir...), retrieve its output and then iterate over the lines in this output, the original reason for the command is to iterate over a set of files. In this form, the code can be written as
setlocal enabledelayedexpansion
for %%a IN ("*_ah.ttf") DO (
set "fName=%%~na"
echo "!fName!" "!fName:~0,-3!"
)
Now, the for command replaceable parameter will iterate over the indicated set of files. (execute for /? for a list of all the command options).
But as foxidrive points, the problem with delayed expansion are the exclamation signs. Without delayed expansion, they are another normal character, but with delayed expansion they frequently become a problem when a value containig them is assigned/echoed.
A quick test
#echo off
setlocal enabledelayedexpansion
set "test=this is a test^!"
echo ---------------------
set test
echo ---------------------
echo delayed : !test!
echo normal : %test%
for /f "delims=" %%a in ("!test!") do echo for : %%a
Will show
---------------------
test=this is a test!
---------------------
delayed : this is a test!
normal : this is a test
for : this is a test
Obviously when the value is a file name, this behaviour will make the code find or not the file.
Depending on the case different solutions can be used, but usually it involves the activation / desactivation of the delayed expansion behaviour (beware, the endlocal removes any change in environment variables from the previous setlocal).
#echo off
setlocal enabledelayedexpansion
set "test=this is a test^!"
echo ---------------------
set test
echo ---------------------
echo delayed : !test!
rem Commuted to no delayed expansion
setlocal disabledelayedexpansion
echo normal : %test%
endlocal
rem Cancelled the initial enable delayed expansion
for /f "delims=" %%a in ("!test!") do endlocal & echo for : %%a
rem The last endlocal has removed the changes to the variable
echo no data : [%test%]
The usual escape character in dos batch files is caret, ^. However for percent, %, the delimiter for variables, the escape is to double up the percents: %%cd%%. Things change when using parameter extensions inside a for loop. Instead of %%~dpnx0 emitting %~dpnx0, as it will outside the loop, it carries out the substitution, emitting D:\Scripts\foo.py.
Here's a batch file demonstration:
#echo off
echo This is a pipe: ^|
echo Use this var for the current directory: %%cd%%
echo Use this to echo full path of running batch file: %%~dpnx0
for %%a in (foo.py baz.py) do (
echo #python %%~dpnxa ^> %%~na.bat
)
These are the results I get:
This is a pipe: |
Use this var for the current directory: %cd%
Use this to echo full path of running batch file: %~dpnx0
#python d:\Scripts\foo.py > foo.bat
#python d:\Scripts\baz.py > baz.bat
But this is what I want:
This is a pipe: |
Use this var for the current directory: %cd%
Use this to echo full path of running batch file: %~dpnx0
#python %~dpnxa > foo.bat
#python %~dpnxa > baz.bat
I've tried doubling, tripling, and quadrupling the percents as well is interspersing carets throughout, all without success.
It is impossible to prevent a FOR variable expression from expanding. If a FOR is in effect with variable X defined, then the FOR expansion phase will always expand %X.
But you can hide the percent behind another FOR variable :)
The following gives the result you are looking for:
#echo off
echo This is a pipe: ^|
echo Use this var for the current directory: %%cd%%
echo Use this to echo full path of running batch file: %%~dpnx0
for %%P in (%%) do for %%A in (foo.py baz.py) do (
echo #python %%P~dpnxA ^> %%~nA.bat
)
FOR variables have global scope (though they are only accessible within the DO clause). This can lead to an insidious problem. Any time you have a subroutine that uses a percent literal within a FOR loop, then you are at risk of unintended results! A FOR statement issued before your CALL can influence the results of a FOR DO clause within your CALLed routine.
#echo off
for %%) in (Yikes!) do call :test
exit /b
:test
echo This works outside loop (confidence = 100%%)
for %%A in (1) do echo This does not work inside loop (confidence = 100%%)
for %%P in (%%) do for %%A in (1) do echo This works inside loop (confidence = 100%%P)
exit /b
Here is the output
This works outside loop (confidence = 100%)
This does not work inside loop (confidence = 100Yikes
This works inside loop (confidence = 100%)
You could use delayed expansion or call-percent expansion, or like dbenham shows expanding another FOR variable.
setlocal EnableDelayedExpansion
set percent=%%
set "line=%%~dpnxa ^>"
for %%a in (foo.py baz.py) do (
echo #python !percent!~dpnxa ^> %%~na.bat
call echo #python %%line%% %%~nxa.bat
)