This question already has an answer here:
Variables are not behaving as expected
(1 answer)
Closed 3 years ago.
I want to read a text file where every line has a number. Negative numbers need to be replaced by 0 and written to a new file along with the rest of the positive numbers.
The problem is that I want to save the value of a line, i.e. %%a into a new variable. Then I'll check if the first character of that variable is '-' if so, I'll set the value to 0 for that line in the final file if not, it will remain as is.
But I cannot save the line value into anything. Below is my code.
3.txt is the original file, tempfile.txt is the final file.
set filem=3.txt
set tempfile=tempfile.txt
for /f "tokens=*" %%a in (%filem%) do (
set linevalue=%%a
IF %linevalue:~0,1% EQU - (
echo 0>>%tempfile%
) ELSE (
echo %%a>>%tempfile%
)
)
pause
If your numbers are really integers, you could give the following method a try:
#Echo Off
Set "filem=3.txt"
Set "tempfile=tempfile.txt"
If Exist "%filem%" (
( For /F "Tokens=1* Delims=]" %%G In (
'""%__AppDir__%find.exe" /N /V ""<"%filem%""'
) Do If "%%H" == "" (
Echo=%%H
) Else (
If %%H Lss 0 (
Echo 0
) Else Echo %%H
)
)>"%tempfile%"
)
Pause
If they're floating point numbers, you'd be both setting and using variables within the same code block, so you would need to use delayed expansion, (which was missing from your code):
#Echo Off
SetLocal DisableDelayedExpansion
Set "filem=3.txt"
Set "tempfile=tempfile.txt"
If Exist "%filem%" (
( For /F "Tokens=1* Delims=]" %%G In (
'""%__AppDir__%find.exe" /N /V ""<"%filem%""'
) Do If "%%H" == "" (
Echo=%%H
) Else (
Set "LineVal=%%H"
SetLocal EnableDelayedExpansion
If "!LineVal:~,1!" == "-" (
Echo 0
) Else Echo %%H
EndLocal
)
)>"%tempfile%"
)
Pause
I am assuming that you will run the bat/cmd in the same directory/folder as your code files, otherwise, edit the cd /d "%~dp0" adding the full/complete folder of your files.
#echo off
cd /d "%~dp0"
set "filem=.\3.txt"
type nul >".\tempfile.txt"
set "tempfile=.\tempfile.txt"
for /f "tokens=1*" %%a in ('type "%filem%"')do (
echo["%%~a"|%__APPDIR__%findstr.exe \-[0-9] >nul && (
echo[0>>"%tempfile%" ) || ( echo/%%~a%%b>>"%tempfile%" )
)
goto :EOF
I am trying to search through a text file for keywords, then insert a number of lines after a specific line/keyword (not end of file).
My code can find the keywords, however I am struggling to add the lines. My code adds the line to the end of the file, so the bit I need help with is after :ADD THE TEXT.
myfile.text looks like:
QFU;
text2;
LastUpdate=20180323;
text3;
I would like to add a list of static lines after LastUpdate, which makes the file look like:
QFU;
text2;
LastUpdate=20180323;
Inserted text1
Inserted text2
text3;
This is my code:
#echo
SET /A COND1=0
for /F "tokens=*" %%i in (myfile.txt) do call :process %%i
goto thenextstep
:process
set VAR1=%1
IF "%VAR1%"=="QFU" SET /A COND1=1
IF "%VAR1%"=="QFU" (
msg * "QFU line found !!"
)
:If QFU line is found then look for Last update
IF "%COND1%"=="1" IF "%VAR1%"=="LastUpdate" (
msg * "LastUpdate line found !!"
:ADD THE TEXT
echo. text to be added>>myfile.txt
:reset COND1 to 0
set /A COND1=0
)
#echo off
setlocal enabledelayedexpansion
call :get_insert_index
if not defined index (
>&2 echo index not defined.
exit /b 1
)
set "i=0"
(
for /f "tokens=*" %%A in (myfile.txt) do (
set /a "i+=1"
echo %%A
for %%B in (%index%) do if !i! equ %%B (
echo --- INSERT
)
)
) > myupdate.txt
exit /b
:get_insert_index
setlocal enabledelayedexpansion
set "i=0"
set "qfu="
set "total="
for /f "tokens=*" %%A in (myfile.txt) do (
set /a i+=1
set "line=%%~A"
if "%%~A" == "QFU;" (
set /a "qfu=!i! + 1"
) else if "!line:~,11!" == "LastUpdate=" (
if defined qfu (
if !i! gtr !qfu! (
if defined total (set total=!total! !i!) else set total=!i!
set "qfu="
)
)
)
)
endlocal & set "index=%total%"
exit /b
This will insert text after the 1st line starting with LastUpdate=,
after the line of QFU;, but not the line starting with LastUpdate=
which is the next line after QFU;.
The label :get_insert_index is called and uses a for loop
to read myfile.txt to get the line index of LastUpdate=
mentioned in the above paragraph.
The variable qfu stores the line index + 1 of QFU; so
LastUpdate= cannot be matched on the next line.
If gfu and LastUpdate= is found and the line index is
greater then gfu, then the line index is appended to total.
qfu is undefined to avoid further matches to LastUpdate=
until QFU; is matched again.
The loop will end and the global variable index is set the
value of total. The label returns control back to the caller.
index is checked if defined at the top of the script after
the call of the label.
The top for loop reads myfile.txt and echoes each line read.
The nested for loop checks the index variable to match the
current line index and if equal, will echo the new text.
The echoes are redirected to myupdate.txt.
Used substitution of "!line:~,11!" so view set /? for help.
Used enabledelayedexpansion so view setlocal /? for help.
Text using ! may find ! being interpreted as a variable
so avoid using !.
Used gtr which can be viewed in if /?. gtr is
"Greater than".
Alternative to avoid creation of an index:
#echo off
setlocal enabledelayedexpansion
set "i=0"
set "gfu="
for /f "tokens=*" %%A in (myfile.txt) do (
set /a i+=1
set "line=%%~A"
>> myupdate.txt echo(%%A
if "%%~A" == "QFU;" (
set /a "qfu=!i! + 1"
) else if "!line:~,11!" == "LastUpdate=" (
if defined qfu (
if !i! gtr !qfu! (
>> myupdate.txt echo --- INSERT
set "qfu="
)
)
)
)
exit /b
>> myupdate.txt echo(%%A writes each line.
>> myupdate.txt echo --- INSERT writes new line to insert.
If system memory permits based on file size, this is much faster:
#echo off
setlocal enabledelayedexpansion
set "i=0"
set "gfu="
(
for /f "tokens=*" %%A in (myfile.txt) do (
set /a i+=1
set "line=%%~A"
echo(%%A
if "%%~A" == "QFU;" (
set /a "qfu=!i! + 1"
) else if "!line:~,11!" == "LastUpdate=" (
if defined qfu (
if !i! gtr !qfu! (
echo --- INSERT
set "qfu="
)
)
)
)
) > myupdate.txt
exit /b
Used on 2.74 MB file, Time reduced from 70s to 21s. The write handle to myupdate.txt remains open for the entire loop, thus the write is cached.
I need to create a batch file to create a folder from substring of a filename
example filename : txt_abc_123
create a folder with the name between "_" (abc)
and the filename with (123)
any help will be much appreciated.
look into the help for FOR /F, something like FOR /F "tokens=2,3* delims=_" %%a in ("txt_abc_123 ") will yield abc in %%a and 123 in %%b.
If you know you only have two underscores in the filename, here's a solution:
#echo off
setlocal enabledelayedexpansion
set xfile=txt_abc_123
set n=0
set zz=0
set before=
set file=
set folder=
:begin
set yy=!xfile:~%n%,1!
set /a n=n+1
if "%yy%"=="_" (
if %zz% EQU 0 (
set zz=1
) ELSE (
set zz=2
)
)
if %zz% EQU 0 (
set before=%before%%yy%
)
if %zz% EQU 1 (
if not "%yy%"=="_" (
set folder=%folder%%yy%
)
)
if %zz% EQU 2 (
if not "%yy%"=="_" (
set file=%file%%yy%
)
)
if not "%yy%"=="" (
goto begin
)
echo Folder: %folder%
echo File: %file%
if not exist %folder%\ (md %folder%)
move %xfile% %folder%\%file%
endlocal
If you don't know you'll have only two underscores, this would need to be modified slightly.
If you want to run this for multiple files, replace the txt_abc_123 above with %1 , save to file batchfilename.bat, and run with
for %i in (*_*_*) do (
batchfilename.bat %i
)
I need to write a script, in.bat, where
the user inputs a 3 digits number (as a string) i.
the script has 4 predefined groups of numbers, number from 1 to 4
the script tests membership for i in the groups, and return the index of the group containing i.
I'm not familiar with declaring and initializing any kinds of data structure (e.g. list, arrays and the like) for batch files, so can someone help me on this?
Pseudo code:
::Returns 1,2,3,4,5 Depending on testNum passed
group1= <822-824,829,845,851,859,864,867>
group2= <826-828,830-839,843-844,847-850,852-854,860-862,883>
group3= <855-858,861,863,865>
group4= <877-882,884>
if %1 is member of group1
return 1
if %1 is member of group2
return 2
if %1 is member of group3
return 3
if %1 is member of group4
return 4
Thank you!
Batch does not have any formal complex data structures like arrays, lists, or objects. But you can emulate them. Here is an efficient solution that defines the groups with nearly the format as in your question.
#echo off
setlocal enableDelayedExpansion
::Here is a small loop to test the routine
for %%N in (822,823,883,835,856,863,880,884) do (
call :assignGroup %%N
echo %%N is in group !group!
)
exit /b
:assignGroup CaseNumber
:: The returning value is contained in variable GROUP
set group=0
for %%A in (
"822-824,829,845,851,859,864,867"
"826-828,830-839,843-844,847-850,852-854,860-862,883"
"855-858,861,863,865"
"877-882,884"
) do (
set /a group+=1
for %%B in (%%~A) do (
for /f "tokens=1,2 delims=-" %%C in ("%%B") do (
if "%%C"=="%~1" exit /b
if "%~1" gtr "%%C" if "%~1" leq "%%D" exit /b
)
)
)
::no group found so undefine the var
set "group="
exit /b
The above solution is fine for occasional calls. But if you were going to call the routine many thousands of times, then it would be better to initialize an array of valid values with assigned group numbers. Then each test becomes a direct read of the value, instead of having to call a routine. However, it is possible to abuse this technique. Assign enough values and each variable assignment gets slower and slower. You might also spend more time setting up the array than actually testing values.
Note there is no significance to the characters []. in the variable names. They could be stripped out of the variable names and the code would function the same. They are there only to aid in the understanding the intent of the variables.
#echo off
setlocal enableDelayedExpansion
::initialize a sparse "array" that assigns a group to each valid case #
set group=0
for %%A in (
"822-824,829,845,851,859,864,867"
"826-828,830-839,843-844,847-850,852-854,860-862,883"
"855-858,861,863,865"
"877-882,884"
) do (
set /a group+=1
for %%B in (%%~A) do (
for /f "tokens=1,2 delims=-" %%C in ("%%B") do (
if "%%D"=="" (
set case[%%C].group=!group!
) else for /l %%N in (%%C 1 %%D) do (
set case[%%N].group=!group!
)
)
)
)
::Now test some values
for %%N in (822,823,883,835,856,863,880,884,900) do (
if defined case[%%N].group (
echo %%N is in !case[%%N].group!
) else (
echo %%N is not in a group
)
)
exit /b
This will set GROUP to whatever group it finds the code in
set test=822,823,824,829,845,851,859,864,867
echo %test% | findstr %1>nul&&set group=1
set test=826,827,828,830,831,832,833,834,835,836,837,838,839,843,844,847,848,849,850,852,853,854,860,861,862,883
echo %test% | findstr %1>nul&&set group=2
set test=855,856,857,858,861,863,865
echo %test% | findstr %1>nul&&set group=3
set test=877,878,879,880,881,882,884
echo %test% | findstr %1>nul&&set group=4
if you want to use ERRORLEVEL to test the return value, then change the SET GROUP= to EXIT /B
By no means perfect, but a working starting point. Save the following script as group.bat and call it with group 878 to find out to which group 878 belongs to.
#echo off
SET group1=822-824,829,845,851,859,864,867
SET group2=826-828,830-839,843-844,847-850,852-854,860-862,883
SET group3=855-858,861,863,865
SET group4=877-882,884
CALL :IsInGroup %1 "%group1%"
IF Errorlevel 1 echo Group 1 & GOTO :EOF
CALL :IsInGroup %1 "%group2%"
IF Errorlevel 1 echo Group 2 & GOTO :EOF
CALL :IsInGroup %1 "%group3%"
IF Errorlevel 1 echo Group 3 & GOTO :EOF
CALL :IsInGroup %1 "%group4%"
IF Errorlevel 1 echo Group 4 & GOTO :EOF
echo Group not found
GOTO :EOF
:IsInGroup
SETLOCAL ENABLEDELAYEDEXPANSION
FOR %%i IN (%~2) DO (
SET h=%%i
SET g=!h:~3,1!
SET /a lo=!h:~0,3!
IF !g!. == -. (
SET /a hi=!h:~4,3!
IF %1 GEQ !lo! (
IF %1 LEQ !hi! exit /B 1
)
) ELSE (
IF %1 EQU !lo! exit /B 1
)
)
ENDLOCAL
EXIT /B 0
The function :IsInGroup checks whether the first argument is contained in the list passed as the second argument.
If the number of values to check for in each group is small, dbenhams second method (direct read of each array value) is the fastest. Any other method that process the values of each group in a FOR loop to do individual or range comparisons is slower. However, as the number of individual elements grows (counting each element included between a range) array value method get slower, as dbenhams indicated.
There is another approach to solve this problem using an aritmethic expression. For example, you may check if a variable is 829 or 845 with this command:
set /A result=(829-variable)*(845-variable)
If the variable have anyone of these two values the result is zero. To check if the variable is into a range, the expression is this:
set /A aux=(lowerLimit-variable)*(variable-upperLimit), result=(aux-1)/aux
Previous expression requires a small provision in case the variable is anyone of the limits (to manage the division by zero). The program below assemble the appropiate aritmethic expression for each of the groups; after that, checking each value is achieved with a maximum of 4 SET /A commands (one per group). This method is faster than individual testing of each value in a FOR loop and use just one variable per group, not per individual element.
#echo off
setlocal EnableDelayedExpansion
rem Assemble testing expression for each group
set numGroup=0
for %%a in (
"822-824,829,845,851,859,864,867"
"826-828,830-839,843-844,847-850,852-854,860-862,883"
"855-858,861,863,865"
"877-882,884"
) do (
set expr=1
for %%b in (%%~a) do (
for /F "tokens=1,2 delims=-" %%c in ("%%b") do (
if "%%d" equ "" (
rem Individual value: multiply previous expr by direct subtract
set "expr=!expr!*(%%c-n^)"
) else (
rem Range value pair: use range expression at this point, then continue
set "expr=!expr!,a=r,r=0,b=(%%c-n^)*(n-%%d),r=(b-1)/b*a"
)
)
)
set /A numGroup+=1
set expr[!numGroup!]=!expr!
)
rem Now test some values
for %%n in (822,823,883,835,855,856,858,863,880,884,900) do (
call :assignGroup %%n
echo %%n is in group !group!
)
goto :EOF
:assignGroup number
set /A n=%1, group=0
for /L %%i in (1,1,%numGroup%) do (
set /A r=!expr[%%i]! 2> NUL
if !r! equ 0 set group=%%i & exit /B 0
)
exit /B 1