Code Excerpt of my batch file:
set stringOne=ABCDEF
echo %stringOne:~2,3%
This output is CDE
How can I dynamically echo the output for my start index and desired output length?
set stringOne=ABCDEF
set start=2
set len=3
you need two layers of variable expansion. That can be done by delayed expansion or by call:
#echo off
setlocal enabledelayedexpansion
set "string=ABCDEFGH"
set "start=2"
set "len=3"
echo A with delayed expansion: !string:~%start%,%len%!
call echo A with using 'call': %%string:~%start%,%len%%%
FOR /F %%G IN ('dir /b "%~f0"') DO (
set /A "newStart=!Start!+2"
call echo B with 'call' and delayed : %%string:~!newStart!,!len!%%
call call echo B with double-'call': %%%%string:~%%newStart%%,%len%%%%%
)
FOR /F %%G IN ('dir /b "%~f0"') DO call :output
goto :eof
:output
set /A "newStart=Start+2"
echo C with subroutine and delayed expansion: !string:~%newStart%,%len%!
call echo C with subroutine andusing 'call': %%string:~%newStart%,%len%%%
goto :eof
EDITED to match your comment. You need a third layer of expansion. I expanded the code with some different methods.
(btw: please don't post code in comments, it's nearly impossible to read. And if your question changes, better ask a follow-up question next time)
I'm manipulating some HTML via Batch. I'm using delayed expansion but when a semicolon is encountered inside double quotes, the script fails to copy anything after and including the semicolon.
It's probably due to the fact that I use double quotes when I pass the variable to the putLineInHTMLFile label (I need to keep things separated in labels).
#echo off
setlocal enableDelayedExpansion
del output.html
for /f "delims=" %%x in (file.html) do call :putLineInHTMLFile "%%x"
goto :EOF
:putLineInHTMLFile
set "line=%~1"
echo !line!>> output.html
file.html contains:
<tag1>
<tag"bla;2">
After running the script, output.html contains:
<tag1>
<tag"bla
I've tried escaping the semicolon with ^ or ^^. Didn't work. I've tried escaping the double quotes too. That didn't work either.
I can change the contents of file.html anyway I please just as long as I can include that semicolon in the output file.
This seems to work for the test-case given; no guarantees for wider use:
#echo off
setlocal enableDelayedExpansion
del output.html
for /f "delims=# tokens=*" %%x in (file.html) do (
set "safe=%%x"
set "safe=!safe:"=""!"
call :putLineInHTMLFile "!safe!"
)
goto :eof
:putLineInHTMLFile
set "line=%~1"
set "line=%line:""="%"
echo !line!>> output.html
:eof
Within the "body" of the for command, the %%x has not been split, it's only when processed by the call command that this happens. To protect that, I've used safe to double-up all double-quotes in the string, and then added a line in the subroutine to strip them out again.
This doesn't work properly if the double-quotes aren't matched, but in those cases, neither does the echoing of the trailing >, even when there are no semi-colons present.
This method works in all cases, as long as the quotes be matched (even number) in the input lines:
#echo off
setlocal enableDelayedExpansion
del output.html
for /f "delims=" %%x in (file.html) do call :putLineInHTMLFile "%%x"
goto :EOF
:putLineInHTMLFile
set line=%*
echo !line:~1,-1!>> output.html
PS - Please, do not include additional parts that not appears in the original code, like the :eof label... The label is written in uppercase letters in goto :EOF command to indicate that it is special. Type goto /? for further details.
Certainly. The problem is that the subroutine receives
"<tag"bla;2">"
for that line. The parser sees that as
Token1 : "<tag"bla
Token2 : 2">"
because ; like Space is a separator.
Sadly, without knowing what you intend to do within the subroutine, it's difficult to advise on how to circumvent the problem.
#echo off
SETLOCAL ENABLEDELAYEDEXPANSION
SET "sourcedir=U:\sourcedir"
SET "destdir=U:\destdir"
SET "filename1=%sourcedir%\q43391363.txt"
SET "outfile=%destdir%\outfile.txt"
SET "outfile2=%destdir%\outfile2.txt"
del "%outfile%"
del "%outfile2%"
(for /f "delims=" %%x in (%filename1%) do set "line=%%x"&call :putLineInHTMLFile "%%x")>"%outfile2%"
goto :eof
:putLineInHTMLFile
ECHO %*
echo !line!>>"%outfile%"
GOTO :EOF
You would need to change the settings of sourcedir and destdir to suit your circumstances.
I used a file named q43391363.txt containing your data for my testing.
Produces files defined as %outfile% and %outfile2%
So - here's two different ways, one using conventional output direct to outfile1 and the other using redirection from a subroutine into outfile2.
Thanks to this community I have finally learned how to escape exlamation marks for immediate use in a batch delayedExpansion block.
(use two escape carets not just one, awesome)
But I can't seem to find or figure out how to pass the contents of a variable containing an exclamation mark as parameter to a batch subroutine.
example:
#echo off
setLocal EnableDelayedExpansion
set variable=Hello^^!
echo "!variable!"
call :subroutine "!variable:^^!=^^!!"
pause
exit
:subroutine
echo "%~1"
exit/b
Output:
"Hello!"
"Hello"
Press any key to continue . . .
I want the second "Hello" to include an exclamation mark.
I have tried various permutations of substring replacement on line 5 to no avail.
help
You need a different way for the variable replacing, and much more carets.
#echo off
setLocal EnableDelayedExpansion
set variable=Hello^^!
echo "!variable!"
call :subroutine %variable:!=^^^^^^^^^^!%
exit /b
:subroutine
echo %~1
exit /b
Or with quotes:
call :subroutine "%variable:!=^^^!%"
In your function you need to expand %1 without any quotes, as the number of carets are always odd in a CALL parameter.
But at all it's a bad idea to try such things.
I agree with Aacini, that you should use pass by reference instead.
This is the only way to handle any possible content.
#echo off
setLocal EnableDelayedExpansion
set variable=Hello^^!
echo "!variable!"
call :subroutine variable
exit /b
:subroutine
echo !%1!
exit /b
Maybe the problem is not how to pass the data to the subroutine, but how to get the data inside it
#echo off
setlocal enabledelayedexpansion
set "var=Hello^!"
setlocal disabledelayedexpansion
echo %var%
call :echo1 %var%
call :echo2 var
endlocal
setlocal enabledelayedexpansion
echo !var!
call :echo1 !var!
call :echo2 var
endlocal
endlocal
exit /b
:echo1
setlocal disabledelayedexpansion
echo %~1
endlocal
goto :eof
:echo2
setlocal enabledelayedexpansion
echo !%~1!
endlocal
goto :eof
I am trying to pass file names as FOR loop parameters to a separate batch file. The problem is, if a file name contains special characters (especially %), the parameter doesnt go to the called script. EG -
The FIRST_SCRIPT.bat is as follows -
cd "C:\theFolder"
for /R %%a in (*.*) do call SECOND_SCRIPT "%%~a"
The SECOND_SCRIPT.bat is as follows -
ECHO %1
If a file name contains % eg. "% of STATS.txt", the output ends up being
of STATS.txt
Which is wrong. I have tried using Setlocal DisableDelayedExpansion but with little success
Setlocal DisableDelayedExpansion
for /R %%a in (*.*) do (
SET "var=%%~a"
Setlocal EnableDelayedExpansion
call TEST_UPGRADE "%var%" "%%~a"
)
There are other stackoverflow answers, but they all need the % character to be known before hand. Since the file names are not in our control, these solutions won't work for us. Is there any way of handling this?
Thanks!
platform: cmd.exe for Windows XP
Aacini shows a solution that would work with % and also ! but it fails with carets ^.
But the solution is simple.
First it's necessary to disable the delayed expansion to handle the exclamation marks.
The filename is now exactly in the var variable.
The problems with carets and percents are caused by the CALL.
This can be solved with the CALL itself by use only the second percent expansion phase of the CALL by using %%var%%.
Setlocal DisableDelayedExpansion
for /R %%a in (*.*) do (
SET "var=%%~a"
call TEST_UPGRADE "%%var%%"
)
The next problem is in the second.bat to display the filename.
This should be done with delayed expansion enabled to avoid problems with special characters, or you need always quotes.
set "var=%~1"
setlocal EnableDelayedExpansion
echo Filename: !var!
solution with a temp file:
first.bat
#ECHO OFF &SETLOCAL
REM to escape the '%' use a second '%'
SET "var=40%% &off!.txt"
REM get a random temp file name
:loop
SET "tname=%temp%%random%%random%"
IF EXIST "%tname%" GOTO :loop
SETLOCAL ENABLEDELAYEDEXPANSION
REM save the variable in the file
>"%tname%" (ECHO(!var!)
CALL "second.bat" "%tname%"
ENDLOCAL
second.bat
#ECHO OFF &SETLOCAL
SET "tname=%~1"
<"%tname%" set/p"var="
SETLOCAL ENABLEDELAYEDEXPANSION
ECHO !var!
DEL "%tname%" /F /Q
..output is:
40% &off!.txt
EDIT: I added the enable/disable delayed expansion technique to avoid problems with exclamation-mark character.
#echo off
setlocal DisableDelayedExpansion
for /F "delims=" %%a in ('dir /B *.txt') do echo %%a
echo/
for %%a in (*.txt) do (
SET "var=%%a"
setlocal EnableDelayedExpansion
call :TEST_UPGRADE "!var:%%=%%%%!" "%%~a"
endlocal
)
goto :EOF
:TEST_UPGRADE
ECHO First: %1 Second: %2
exit /B
Output example:
% of STATS.txt
Discount of 10% in all.txt
Normal file.txt
First: "% of STATS.txt" Second: " of STATS.txt"
First: "Discount of 10% in all.txt" Second: "Discount of 10 in all.txt"
First: "Normal file.txt" Second: "Normal file.txt"
I have a batch file that computes a variable via a series of intermediate variables:
#echo off
setlocal
set base=compute directory
set pkg=compute sub-directory
set scripts=%base%\%pkg%\Scripts
endlocal
%scripts%\activate.bat
The script on the last line isn't called, because it comes after endlocal, which clobbers the scripts environment variable, but it has to come after endlocal because its purpose is to set a bunch of other environment variables for use by the user.
How do I call a script who's purpose is to set permanent environment variables, but who's location is determined by a temporary environment variable?
I know I can create a temporary batch file before endlocal and call it after endlocal, which I will do if nothing else comes to light, but I would like to know if there is a less cringe-worthy solution.
The ENDLOCAL & SET VAR=%TEMPVAR% pattern is classic. But there are situations where it is not ideal.
If you do not know the contents of TEMPVAR, then you might run into problems if the value contains special characters like < > & or|. You can generally protect against that by using quotes like SET "VAR=%TEMPVAR%", but that can cause problems if there are special characters and the value is already quoted.
A FOR expression is an excellent choice to transport a value across the ENDLOCAL barrier if you are concerned about special characters. Delayed expansion should be enabled before the ENDLOCAL, and disabled after the ENDLOCAL.
setlocal enableDelayedExpansion
set "TEMPVAR=This & "that ^& the other thing"
for /f "delims=" %%A in (""!TEMPVAR!"") do endlocal & set "VAR=%%~A"
Limitations:
If delayed expansion is enabled after the ENDLOCAL, then the final value will be corrupted if the TEMPVAR contained !.
values containing a lineFeed character cannot be transported
If you must return multiple values, and you know of a character that cannot appear in either value, then simply use the appropriate FOR /F options. For example, if I know that the values cannot contain |:
setlocal enableDelayedExpansion
set "temp1=val1"
set "temp2=val2"
for /f "tokens=1,2 delims=|" %%A in (""!temp1!"|"!temp2!"") do (
endLocal
set "var1=%%~A"
set "var2=%%~B"
)
If you must return multiple values, and the character set is unrestricted, then use nested FOR /F loops:
setlocal enableDelayedExpansion
set "temp1=val1"
set "temp2=val2"
for /f "delims=" %%A in (""!temp1!"") do (
for /f "delims=" %%B in (""!temp2!"") do (
endlocal
set "var1=%%~A"
set "var2=%%~B"
)
)
Definitely check out jeb's answer for a safe, bullet proof technique that works for all possible values in all situations.
2017-08-21 - New function RETURN.BAT
I've worked with DosTips user jeb to develop a batch utility called RETURN.BAT that can be used to exit from a script or called routine and return one or more variables across the ENDLOCAL barrier. Very cool :-)
Below is version 3.0 of the code. I most likely will not keep this code up-to-date. Best to follow the link to make sure you get the latest version, and to see some example usage.
RETURN.BAT
::RETURN.BAT Version 3.0
#if "%~2" equ "" (goto :return.special) else goto :return
:::
:::call RETURN ValueVar ReturnVar [ErrorCode]
::: Used by batch functions to EXIT /B and safely return any value across the
::: ENDLOCAL barrier.
::: ValueVar = The name of the local variable containing the return value.
::: ReturnVar = The name of the variable to receive the return value.
::: ErrorCode = The returned ERRORLEVEL, defaults to 0 if not specified.
:::
:::call RETURN "ValueVar1 ValueVar2 ..." "ReturnVar1 ReturnVar2 ..." [ErrorCode]
::: Same as before, except the first and second arugments are quoted and space
::: delimited lists of variable names.
:::
::: Note that the total length of all assignments (variable names and values)
::: must be less then 3.8k bytes. No checks are performed to verify that all
::: assignments fit within the limit. Variable names must not contain space,
::: tab, comma, semicolon, caret, asterisk, question mark, or exclamation point.
:::
:::call RETURN init
::: Defines return.LF and return.CR variables. Not required, but should be
::: called once at the top of your script to improve performance of RETURN.
:::
:::return /?
::: Displays this help
:::
:::return /V
::: Displays the version of RETURN.BAT
:::
:::
:::RETURN.BAT was written by Dave Benham and DosTips user jeb, and was originally
:::posted within the folloing DosTips thread:
::: http://www.dostips.com/forum/viewtopic.php?f=3&t=6496
:::
::==============================================================================
:: If the code below is copied within a script, then the :return.special code
:: can be removed, and your script can use the following calls:
::
:: call :return ValueVar ReturnVar [ErrorCode]
::
:: call :return.init
::
:return ValueVar ReturnVar [ErrorCode]
:: Safely returns any value(s) across the ENDLOCAL barrier. Default ErrorCode is 0
setlocal enableDelayedExpansion
if not defined return.LF call :return.init
if not defined return.CR call :return.init
set "return.normalCmd="
set "return.delayedCmd="
set "return.vars=%~2"
for %%a in (%~1) do for /f "tokens=1*" %%b in ("!return.vars!") do (
set "return.normal=!%%a!"
if defined return.normal (
set "return.normal=!return.normal:%%=%%3!"
set "return.normal=!return.normal:"=%%4!"
for %%C in ("!return.LF!") do set "return.normal=!return.normal:%%~C=%%~1!"
for %%C in ("!return.CR!") do set "return.normal=!return.normal:%%~C=%%2!"
set "return.delayed=!return.normal:^=^^^^!"
) else set "return.delayed="
if defined return.delayed call :return.setDelayed
set "return.normalCmd=!return.normalCmd!&set "%%b=!return.normal!"^!"
set "return.delayedCmd=!return.delayedCmd!&set "%%b=!return.delayed!"^!"
set "return.vars=%%c"
)
set "err=%~3"
if not defined err set "err=0"
for %%1 in ("!return.LF!") do for /f "tokens=1-3" %%2 in (^"!return.CR! %% "") do (
(goto) 2>nul
(goto) 2>nul
if "^!^" equ "^!" (%return.delayedCmd:~1%) else %return.normalCmd:~1%
if %err% equ 0 (call ) else if %err% equ 1 (call) else cmd /c exit %err%
)
:return.setDelayed
set "return.delayed=%return.delayed:!=^^^!%" !
exit /b
:return.special
#if /i "%~1" equ "init" goto return.init
#if "%~1" equ "/?" (
for /f "tokens=* delims=:" %%A in ('findstr "^:::" "%~f0"') do #echo(%%A
exit /b 0
)
#if /i "%~1" equ "/V" (
for /f "tokens=* delims=:" %%A in ('findstr /rc:"^::RETURN.BAT Version" "%~f0"') do #echo %%A
exit /b 0
)
#>&2 echo ERROR: Invalid call to RETURN.BAT
#exit /b 1
:return.init - Initializes the return.LF and return.CR variables
set ^"return.LF=^
^" The empty line above is critical - DO NOT REMOVE
for /f %%C in ('copy /z "%~f0" nul') do set "return.CR=%%C"
exit /b 0
#ECHO OFF
SETLOCAL
REM Keep in mind that BAR in the next statement could be anything, including %1, etc.
SET FOO=BAR
ENDLOCAL && SET FOO=%FOO%
The answer of dbenham is a good solution for "normal" strings, but it fails with exclamation marks ! if delayed expansion is enabled after ENDLOCAL (dbenham said this too).
But it will always fail with some tricky contents like embedded linefeeds,
as the FOR/F will split the content into multiple lines.
This will result into strange behaviour, the endlocal will executed multiple times (for each line feed), so the code isn't bullet proof.
There exists bullet proof solutions, but they are a bit messy :-)
A macro version exists SO:Preserving exclamation ..., to use it is easy, but to read it is ...
Or you could use a code block, you can paste it into your functions.
Dbenham and I developed this technic in the thread Re: new functions: :chr, :asc, :asciiMap,
there are also the explanations for this technic
#echo off
setlocal EnableDelayedExpansion
cls
for /f %%a in ('copy /Z "%~dpf0" nul') do set "CR=%%a"
set LF=^
rem TWO Empty lines are neccessary
set "original=zero*? %%~A%%~B%%~C%%~L!LF!one&line!LF!two with exclam^! !LF!three with "quotes^&"&"!LF!four with ^^^^ ^| ^< ^> ( ) ^& ^^^! ^"!LF!xxxxxwith CR!CR!five !LF!six with ^"^"Q ^"^"L still six "
setlocal DisableDelayedExpansion
call :lfTest result original
setlocal EnableDelayedExpansion
echo The result with disabled delayed expansion is:
if !original! == !result! (echo OK) ELSE echo !result!
call :lfTest result original
echo The result with enabled delayed expansion is:
if !original! == !result! (echo OK) ELSE echo !result!
echo ------------------
echo !original!
goto :eof
::::::::::::::::::::
:lfTest
setlocal
set "NotDelayedFlag=!"
echo(
if defined NotDelayedFlag (echo lfTest was called with Delayed Expansion DISABLED) else echo lfTest was called with Delayed Expansion ENABLED
setlocal EnableDelayedExpansion
set "var=!%~2!"
rem echo the input is:
rem echo !var!
echo(
rem ** Prepare for return
set "var=!var:%%=%%~1!"
set "var=!var:"=%%~2!"
for %%a in ("!LF!") do set "var=!var:%%~a=%%~L!"
for %%a in ("!CR!") do set "var=!var:%%~a=%%~3!"
rem ** It is neccessary to use two IF's else the %var% expansion doesn't work as expected
if not defined NotDelayedFlag set "var=!var:^=^^^^!"
if not defined NotDelayedFlag set "var=%var:!=^^^!%" !
set "replace=%% """ !CR!!CR!"
for %%L in ("!LF!") do (
for /F "tokens=1,2,3" %%1 in ("!replace!") DO (
ENDLOCAL
ENDLOCAL
set "%~1=%var%" !
#echo off
goto :eof
)
)
exit /b
I want to contribute to this too and tell you how you can pass over an array-like set of variables:
#echo off
rem clean up array in current environment:
set "ARRAY[0]=" & set "ARRAY[1]=" & set "ARRAY[2]=" & set "ARRAY[3]="
rem begin environment localisation block here:
setlocal EnableExtensions
rem define an array:
set "ARRAY[0]=1" & set "ARRAY[1]=2" & set "ARRAY[2]=4" & set "ARRAY[3]=8"
rem `set ARRAY` returns all variables starting with `ARRAY`:
for /F "tokens=1,* delims==" %%V in ('set ARRAY') do (
if defined %%V (
rem end environment localisation block once only:
endlocal
)
rem re-assign the array, `for` variables transport it:
set "%%V=%%W"
)
rem this is just for prove:
for /L %%I in (0,1,3) do (
call echo %%ARRAY[%%I]%%
)
exit /B
The code works, because the very first array element is queried by if defined within the setlocal block where it is actually defined, so endlocal is executed once only. For all the successive loop iterations, the setlocal block is already ended and therefore if defined evaluates to FALSE.
This relies on the fact that at least one array element is assigned, or actually, that there is at least one variable defined whose name starts with ARRAY, within the setlocal/endlocal block. If none exist therein, endlocal is not going to be executed. Outside of the setlocal block, no such variable must be defined, because otherwise, if defined evaluates to TRUE more than once and therefore, endlocal is executed multiple times.
To overcome this restrictions, you can use a flag-like variable, according to this:
clear the flag variable, say ARR_FLAG, before the setlocal command: set "ARR_FLAG=";
define the flag variable inside of the setlocal/endlocal block, that is, assign a non-empty value to it (immediately before the for /F loop preferrably): set "ARR_FLAG=###";
change the if defined command line to: if defined ARR_FLAG (;
then you can also do optionally:
change the for /F option string to "delims=";
change the set command line in the for /F loop to: set "%%V";
Something like the following (I haven't tested it):
#echo off
setlocal
set base=compute directory
set pkg=compute sub-directory
set scripts=%base%\%pkg%\Scripts
pushd %scripts%
endlocal
call .\activate.bat
popd
Since the above doesn't work (see Marcelo's comment), I would probably do this as follows:
set uniquePrefix_base=compute directory
set uniquePrefix_pkg=compute sub-directory
set uniquePrefix_scripts=%uniquePrefix_base%\%uniquePrefix_pkg%\Scripts
set uniquePrefix_base=
set uniquePrefix_pkg=
call %uniquePrefix_scripts%\activate.bat
set uniquePrefix_scripts=
where uniquePrefix_ is chosen to be "almost certainly" unique in your environment.
You could also test on entry to the bat file that the "uniquePrefix_..." environment variables are undefined on entry as expected - if not you can exit with an error.
I don't like copying the BAT to the TEMP directory as a general solution because of (a) the potential for a race condition with >1 caller, and (b) in the general case a BAT file might be accessing other files using a path relative to its location (e.g. %~dp0..\somedir\somefile.dat).
The following ugly solution will solve (b):
setlocal
set scripts=...whatever...
echo %scripts%>"%TEMP%\%~n0.dat"
endlocal
for /f "tokens=*" %%i in ('type "%TEMP%\%~n0.dat"') do call %%i\activate.bat
del "%TEMP%\%~n0.dat"
For surviving multiple variables: If you choose to go with the "classic"
ENDLOCAL & SET VAR=%TEMPVAR% mentioned sometimes in other responses here (and are satisfied that the drawbacks shown in some of the responses are addressed or are not an issue), note that you can do multiple variables, a la ENDLOCAL & SET var1=%local1% & SET var2=%local2%.
I share this because other than the linked site below, I have only seen the "trick" illustrated with a single variable, and like myself some may have incorrectly assumed that it only "works" for a single variable.
Docs: https://ss64.com/nt/endlocal.html
To answer my own question (in case no other answer comes to light, and to avoid repeats of the one I already know about)...
Create a temporary batch file before calling endlocal that contains the command to call the target batch file, then call and delete it after endlocal:
echo %scripts%\activate.bat > %TEMP%\activate.bat
endlocal
call %TEMP%\activate.bat
del %TEMP%\activate.bat
This is so ugly, I want to hang my head in shame. Better answers are most welcome.
How about this.
#echo off
setlocal
set base=compute directory
set pkg=compute sub-directory
set scripts=%base%\%pkg%\Scripts
(
endlocal
"%scripts%\activate.bat"
)