Batch random line for a .txt file - batch-file

I'm writing a batch file and I wanted to display a splash in the main screen. I wanted the batch file to pick a random line from a text file and ECHO it. The file name would be splashes.txt.
And in it would be like:
More addicting the lemons
Apples.
This is a splash
Test
The batch file would pick a random quote based on its line.
Like if the patch file picked line 2 it would ECHO "Apples."
Note that I'm using Windows 8.
Any ideas?

While I agree with others that you should attempt this on your own, this is a relatively straight-forward script which should serve as a good working example for learning:
#ECHO OFF
SETLOCAL EnableDelayedExpansion EnableExtensions
REM Source file.
REM The first line on this file should be blank as it will never be selected.
REM Additionally, this file should have no empty lines on the end.
SET TextFile=text.txt
REM Determine the number of lines.
SET NumLines=0
FOR /F "usebackq tokens=* delims=" %%A IN (`TYPE %TextFile%`) DO SET /A NumLines=!NumLines!+1
REM Pick a random line.
SET /A RandomLine=(%RANDOM% %% %NumLines) + 1
REM Prevent skipping all the lines.
IF "%RandomLine%"=="%NumLines%" SET RandomLine=1
REM Print the random line.
FOR /F "usebackq tokens=* skip=%RandomLine% delims=" %%A IN (`TYPE %TextFile%`) DO (
ECHO %%A
REM We are done. Stop the script.
GOTO Finish
)
:Finish
ENDLOCAL
So close - but not quite. SKIP will always be at least 1 (since SKIP=0 is invalid) hence the first line in the file can never be selected.
This is a file I derived from the above with a few tickles. I've also changed the filename because of the way I work. I'm using q27829742.txt containing the lines posted.
#ECHO OFF
SETLOCAL
SETLOCAL EnableDelayedExpansion EnableExtensions
REM Source file.
REM The first line on this file should be blank as it will never be selected.
REM Additionally, this file should have no empty lines on the end.
SET "TextFile=q27829742.txt"
REM Determine the number of lines.
FOR /f %%a IN ('type "%textfile%"^|find /c /v ""') DO SET /a numlines=%%a
REM Pick a random line.
SET /A RandomLine=(%RANDOM% %% %NumLines%)
REM Prevent skipping all the lines.
IF "%RandomLine%"=="0" (SET "RandomLine=") ELSE (SET "RandomLine=skip=%randomline%")
REM Print the random line.
FOR /F "usebackq tokens=* %RandomLine% delims=" %%A IN (`TYPE %TextFile%`) DO (
ECHO %%A
REM We are done. Stop the script.
GOTO Finish
)
:Finish
ENDLOCAL
The find /v /c method counts lines in the file (find files lines that don't match (/v) "" (so that means all lines) and count them (/c) - simply more efficient.
Pick-random-number : removing the + 1 produces a result 0..(numlines-1) which is the actual number of lines to skip.
Problem there is that skip=0 is invalid, so construct a string which is eother empty (for 0) or "skip=..." (otherwise) - all ready to be included in the for /f command-options.
The syntax SET "var=value" (where value may be empty) is used to ensure that any stray spaces at the end of a line are NOT included in the value assigned. set /a can safely be used "quoteless".

If you know the number of lines to be, let's say 25.
You could have a vector holding each line, and then compute a random index and print out the corresponding line with the following example.
setlocal EnableDelayedExpansion
SET line[0]=Apples.
SET line[1]=This is a splash
SET line[2]=Test
SET line[3]=...
SET line[24]=many lines later...
SET /A index=%RANDOM% % 25
ECHO !line[index]!

A practical application of Jason Falkner's (and Magoo's) answer would be to their script and place it into a .cmd file that would reside in System32 (say randomline.cmd). This will integrate it into the command line utility, making it easily re-usable via a simple call line, avoiding you to have rewrite those lines over and over and over again in all your scripts. For example:
if you call the Magoo/Falkner script randomline.cmd and place it in System32
randomline.cmd
#ECHO OFF
SETLOCAL
SETLOCAL EnableDelayedExpansion EnableExtensions
REM Source file.
REM The first line on this file should be blank as it will never be selected.
REM Additionally, this file should have no empty lines on the end.
SET "TextFile=%1"
REM Determine the number of lines.
FOR /f %%a IN ('type "%textfile%"^|find /c /v ""') DO SET /a numlines=%%a
REM Pick a random line.
SET /A RandomLine=(%RANDOM% %% %NumLines%)
REM Prevent skipping all the lines.
IF "%RandomLine%"=="0" (SET "RandomLine=") ELSE (SET "RandomLine=skip=%randomline%")
REM Print the random line.
FOR /F "usebackq tokens=* %RandomLine% delims=" %%A IN (`TYPE %TextFile%`) DO (
ECHO %%A
REM We are done. Stop the script.
GOTO Finish
)
:Finish
ENDLOCAL
you'll be able to use it anypoint in the command prompt by entering
randomline any_old_text_file.txt
or if you want to use it in a cmd/bat script
call randomline any_old_text_file.txt
And if you want to set a value for a variable in your script from a random line in a file, you can do:
call radomline file_containing_values_on_each_line.txt>%temp%\randomlinevalue.txt
set /p any_old_variable=<%temp%\randomlinevalue.txt
del %temp%\randomlinevalue.txt

Related

Using a filename variable in a for loop

I have a directory with a file in it (actually it has a lot of files, but I figured getting one to work was the first step before building the loop to hit each file), that needs to be edited and saved with a similar filename. Instead of manually typing in the filename, I'd like to use a variable containing the filename.
Sample Input Data
FileX.txt
Text line
Text line
Text line
Desired Output Data
FileX2.txt
1 Text line
2 Text line
3 text line
I can manage this with one file at a time, I'm struggling to write one script to look through the contents of a folder and do this to each text file within the folder.
I'm on windows 7 and this is what I have so far:
#echo off
setLocal EnableDelayedExpansion
Set N=0
REM THE BELOW LINE IS THE VARIABLE I'M TRYING TO SET AND THEN HAVE PASSED AS THE FOR LOOP PARAMETER
Set F="FILENAME"
REM IF I SKIP A VARIABLE AND HARDCODE THE FILENAME IN THE BELOW LINE, IT WORKS FOR THE ONE FILE
for /f "tokens=* delims= " %%a in (FILENAMEVARIABLE.txt) do (
Set /a N=!N!+1
echo !N! %%a, >> !F!.txt
)
The opening statement of the for loop, is where I can't get the variable to take. In FILENAMEVARIABLE.txt I have tried %%F, %F, !F!, and %%~nxf none of which manage to call the correct file to start the loop. Any ideas what I'm doing wrong here?
Here's what I think you were trying to do:
#Echo Off
SetLocal EnableDelayedExpansion
For %%A In (*.txt) Do (Set "N=0"
(For /F "UseBackQ Tokens=*" %%B In ("%%A") Do (Set /A N +=1
Echo !N! %%B))>"%%~nA2%%~xA")
You could also use FindStr:
#Echo Off
For /F "Tokens=1-2* Delims=:" %%A In ('FindStr /N "^" *.txt 2^Nul'
) Do >>"%%~nA2%%~xA" Echo %%B %%C
In both of the above examples I have assumed that the source directory and script directory are the same.

batch for loop in file

I have a Batch script :
#echo off
setlocal enableDelayedExpansion
SET /P UserInput=Please Enter a Number:
SET /A number=UserInput
ECHO number=%number%
for %%i in (*.jpeg) do call :JPG %%~ni %%i
goto :end
:JPG
set str=%1
set /a str2=%str:_color=%
set /a newnamej=%str2%+%number%
echo %1 ==> I can see the problem with it
set lastnamej=%newnamej%_color.jpeg
ren %2 %lastnamej%
goto :eof
:end
The goal of this script is to take all file in a folder. They are all named after a number (1_color.jpeg, 2_color.jpeg, 3_color.jpeg,..) and I want to rename them with an additionnal number (if user input is 5, 1_color.jpeg will become 6_color.jpeg, and so on).
I have a problem with this script.
if I use a number such as 555, the first file will pass in the for loop 2 times.
(little example : 1_color.jpeg and 2_color.jpeg,
I use my script with 5 so 1_color.jpeg => 6_color.jpeg and 2_color.jpeg => 7_color.jpeg but then, 6_color.jpeg will be read again once, and will become 11_color.jpeg, so my result will be 11_color.jpeg and 7_color.jpeg).
Do someone know how to fix this issue?
Thanks for all!
The problem have two parts: the for %%i in (*.jpeg) ... command may be dinamically affected by the position that a renamed file will occupy in the whole file list, so some files may be renamed twice and, in certain particular cases with many files, up to three times.
The solution is to use a for /F %%i in ('dir /B *.jpeg') ... command instead, that first get the list of all files, and then start the renaming process.
Also, the rename must be done from last file to first one order, to avoid duplicate numbers.
However, in this case the use of for /F combined with "tokens=1* delims=_" option also allows to process the first underscore-separated number in the file names in a simpler way:
#echo off
setlocal EnableDelayedExpansion
SET /P number=Please Enter a Number:
ECHO number=%number%
for /F "tokens=1* delims=_" %%a in ('dir /O:-N /B *.jpeg') do (
set /A newNum=%%a+number
ren "%%a_%%b" "!newNum!_%%b"
)
User Aacini provided a nice solution in his answer, pointing out both issues at hand, namely the fact that for does not fully enumerate the directory in advance (see this thread: At which point does for or for /R enumerate the directory (tree)?) and the flaw in the logic concerning the sort order of the processed files.
However, there is still a problem derived from the purely (reverse-)alphabetic sort order of dir /B /O:-N *.jpeg, which can still cause collisions, as the following example illustrates:
9_color.jpeg
8_color.jpeg
7_color.jpeg
6_color.jpeg
5_color.jpeg
4_color.jpeg
3_color.jpeg
2_color.jpeg
10_color.jpeg
1_color.jpeg
So if the entered number was 1, file 9_color.jpeg is tried to be renamed to 10_color.jpeg, which fails because that file already exists as it has not yet been processed (hence renamed to 11_color.jpeg).
To overcome this problem, you need to correctly sort the items in reverse alpha-numeric order. This can be achieved by left-zero-padding the numbers before sorting them, because then, alphabetic and alpha-numeric sort orders match. Here is a possible implementation:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Define constants here:
set "_LOCATION=." & rem // (directory containing the files to rename)
set "_PATTERN=*_*.jpeg" & rem // (search pattern for the files to rename)
set "_REGEX1=^[0-9][0-9]*_[^_].*\.jpeg$" & rem // (`findstr` filter expression)
set "_TEMPFILE=%TEMP%\%~n0_%RANDOM%.tmp" & rem // (path to temporary file)
rem // Retrieve numeric user input:
set "NUMBER="
set /P NUMBER="Please Enter a number: "
set /A "NUMBER+=0"
if %NUMBER% GTR 0 (set "ORDER=/R") else if %NUMBER% LSS 0 (set "ORDER=") else exit /B
rem /* Write `|`-separated list of left-zero-padded file prefixes, original and new
rem file names into temporary file: */
> "%_TEMPFILE%" (
for /F "tokens=1* delims=_" %%E in ('
dir /B "%_LOCATION%\%_PATTERN%" ^| findstr /I /R /C:"%_REGEX1%"
') do (
set "NAME=%%F"
setlocal EnableDelayedExpansion
set "PADDED=0000000000%%E"
set /A "NUMBER+=%%E"
echo !PADDED:~-10!^|%%E_!NAME!^|!NUMBER!_!NAME!
endlocal
)
)
rem /* Read `|`-separated list from temporary file, sort it by the left-zero-padded
rem prefixes, extract original and new file names and perform actual renaming: */
< "%_TEMPFILE%" (
for /F "tokens=2* delims=|" %%K in ('sort %ORDER%') do (
ECHO ren "%%K" "%%L"
)
)
rem // Clean up temporary file:
del "%_TEMPFILE%"
endlocal
exit /B
After having successfully verified the correct output of the script, to not forget to remove the upper-case ECHO command in front of the ren command line.
The script uses a temporary file that receives a |-separated table with the padded numeric prefix in the first, the original file name in the second and the new file name in the third column, like this:
0000000010|10_color.jpeg|11_color.jpeg
0000000001|1_color.jpeg|2_color.jpeg
0000000002|2_color.jpeg|3_color.jpeg
0000000003|3_color.jpeg|4_color.jpeg
0000000004|4_color.jpeg|5_color.jpeg
0000000005|5_color.jpeg|6_color.jpeg
0000000006|6_color.jpeg|7_color.jpeg
0000000007|7_color.jpeg|8_color.jpeg
0000000008|8_color.jpeg|9_color.jpeg
0000000009|9_color.jpeg|10_color.jpeg
The temporary file is read and sorted by the sort command. The strings from the second and third columns are extracted and passed over to the ren command.

Batch to delete the first line of a text file without creating a new file

Is there any solution to this question? I see a lot of questions about deleting first line of a text file but all of them need create a new text file. I need this because my text file is constantly updating with new lines (by a second batch file) so if the script creates a new text file it can accidently delete some new lines with the old text file.
Can i delete first line of a text file without creating a new one? If not, why?
Using only batch.
Blank lines shouldn't be preserved (If possible).
Especial characters like ! can be deleted.
IMO this question makes no sense. There is no way to avoid to preserve the new file contents in a place different than the original file; this is true even in any advanced programming language, that would require to read from second line to end of file and copy each line to the beginning of the file. However, at end of the process it would be necessary to truncate the file in order to eliminate the last bytes in the file (with the number of bytes that the first line had).
A Batch file certainly can not perform this type of process, so the lines of the file (from second one up to the end of file) must necessarily be stored in a place different than the original file. One of the answers store the lines in memory variables, but this method is inefficient specially if the file is large.
So, if the question is: "what tricks can be used in order to not use a new file to eliminate the first line in a data file?", then this is a more efficient method:
#echo off
for /F "skip=1 delims=" %%a in ('type input.txt ^& del input.txt') do >> input.txt echo %%a
If you want to preserve empty lines, use this method instead:
#echo off
for /F "skip=1 tokens=1* delims=:" %%a in ('findstr /N "^" input.txt ^& del input.txt') do >> input.txt echo(%%b
You should note that when the for /F command start execution it blocks the file for an exclusive access, so any attempt to modify the file while the for /F is reading it would be avoided with "access denied" error.
If you don't care about preserving blank lines, !, (, ), or ^, you can run the input file through a for loop, storing each line in a separate variable, then merging the variables with a newline character at the end of each one.
#echo off
setlocal enabledelayedexpansion
cls
set counter=0
for /f "delims=" %%A in (input.txt) do (
set line[!counter!]=%%A
set /a counter+=1
)
set /a counter-=2
set LF=^
for /L %%A in (1,1,!counter!) do set sheet=!sheet!!line[%%A]!!LF!
set /a counter+=1
for /L %%A in (!counter!,1,!counter!) do set sheet=!sheet!!line[%%A]!
echo !sheet!>input.txt
However, if you want to preserve blank lines and special characters, there are a few tricks you can throw in, but the overall idea is the same.
#echo off
setlocal enabledelayedexpansion
cls
:: findstr /n puts line numbers at the start of each line, which will allow us to preserve blank lines
for /f "tokens=1 delims=:" %%A in ('findstr /n "^" input.txt') do set line_counter=%%A
::set /p preserves special characters
<input.txt (
for /L %%A in (1,1,!line_counter!) do set /p line[%%A]=
)
set /a line_counter-=1
set LF=^
:: Do NOT delete the two blank lines above this line.
for /L %%A in (2,1,!line_counter!) do set sheet=!sheet!!line[%%A]!!LF!
set /a line_counter+=1
for /L %%A in (!line_counter!,1,!line_counter!) do set sheet=!sheet!!line[%%A]!
echo !sheet!>input.txt
In general a new file is needed because the requested operation requires that you READ and WRITE. So with only 1 file you would be modifying the file that you are reading! If you are simply concerned about the resultant file being a different file, that can be resolved by deleting the original and renaming the new file when done. The PUSHD and POPD are optional... use if applicable. ** Untested **
pushd "YourFolderName"
set "FileName=YourInFileName.txt"
set "TempFile=SomeTempFileName.txt"
if exist "%TempFile% del /f /q "%TempFile%"
more "%FileName%" +1 > "%TempFile%"
del /f /q "%FileName%"
ren "%TempFile%" "%FileName%"
popd

How to randomly rearrange lines in a text file using a batch file

I am creating a code that strips through different MAC addresses randomly, but cannot figure out how to do this. My thought on how to approach this is to randomize or rearrange the order of the MAC address in the text file with this script, but I cannot figure out how to do this with a batch file. How this will work is that it will read "maclist.txt", then create a new temp file with the random order "maclist_temp.txt", that will be the rearranged file. Then, it will pull this randomized file in order.
I have tried Google and searching the web, but I haven't found anything too useful. I'm still actively looking, but any advice would be extremely useful.
Something as simple as extracting and deleting a random line and then adding to the bottom might work. Randomization would be better though, but I want to keep the original list. Something like:
Make a temp copy of maclist.txt called maclist_temp.txt
Take one random MAC address, remove it from maclist_temp.txt
Readd it to the bottom
That is all I want, but any suggestions are welcome.
You may try this batch file to help you to shuffle your maclist.txt. The usage of the batch code is
C:\> type list.txt | shuffle.bat > maclist_temp.txt
Here are the contents of shuffle.bat:
#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
SET TmpFile=tmp%RANDOM%%RANDOM%.tmp
TYPE NUL >%Tmpfile%
FOR /F "tokens=*" %%i IN ('MORE') DO SET Key=!RANDOM!!RANDOM!!RANDOM!000000000000& ECHO !Key:~0,15!%%i>> %TmpFile%
FOR /F "tokens=*" %%i IN ('TYPE %TmpFile% ^| SORT') DO SET Line=%%i&ECHO.!Line:~15!
::DEL %TmpFile%
ENDLOCAL
After issuing the above command, maclist_temp.txt will contain a randomized list of MAC addresses.
Hope this helps.
Here is a simpler method to randomize/randomise a file, no temp files needed. You can even reuse the same input filename.
Limitations are: blank lines and line starting with ; will be skipped, and lines starting with = will have all leading = signs stripped and ^ characters are doubled.
#echo off
setlocal
for /f "delims=" %%a in (maclist.txt) do call set "$$%%random%%=%%a"
(for /f "tokens=1,* delims==" %%a in ('set $$') do echo(%%b)>newmaclist.txt
endlocal
I really like foxidrive's approach. Nevertheless I want to provide a solution with all the listed limitations eliminated (although cmd-related restrictions like file sizes < 2 GiB and line lengths < ~ 8 KiB remain).
The key is delayed expansion which needs to be toggled to not lose explamation marks. This solves all the potential problems with special characters like ^, &, %, !, (, ), <, >, | and ".
The counter index has been implemented in order not to lose a single line of the original text file, which could happen without, because random may return duplicate values; with index appended, the resulting variable names $$!random!.!index! are unique.
The findstr /N /R "^" command precedes every line of the original file with a line number followed by a colon. So no line appears empty to the for /F loop which would ignore such. The line number also implicitly solves the issue with leading semicolons, the default eol character of for /F.
Finally, everything up to and including the first colon (remember the said prefix added by findstr) is removed from every line before being output, hence no more leading equal-to signs are dismissed.
So here is the code:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set /A "index=0"
for /f "delims=" %%a in ('findstr /N /R "^" "%~dpn0.lst"') do (
setlocal EnableDelayedExpansion
for /F %%b in ("$$!random!.!index!") do (
endlocal
set "%%b=%%a"
)
set /A "index+=1"
)
> "%~dpn0.new" (
for /f "delims=" %%a in ('set $$') do (
set "item=%%a"
setlocal EnableDelayedExpansion
echo(!item:*:=!
endlocal
)
)
endlocal
exit /B
This seems to work. Feed it a command line parameter of the file to randomize.
#echo off
setlocal EnableDelayedExpansion
rem read the number of lines in the file
rem the find prepends the line number so we catch blank lines
for /f "delims=" %%n in ('find /c /v "" %1') do set "len=%%n"
set len=!len:*: =!
rem echo %1 has %len% lines
rem Relocate as many lines as there are lines in the file
for /l %%j in (1 1 !len!) do (
rem echo starting round %%j
rem geta random number between 1 and the number of lines in the file
set /a var=!random! %% !len! + 1
rem echo relocating line !var!
rem make sure there is no temp file
if exist %1.temp del %1.temp
rem read each line of the file, write any that don't match and then write the one that does
<%1 (
for /l %%i in (1 1 !len!) do (
rem if it is the target line then save it
if %%i == !var! (
set /p found=
rem echo saving !found!
)
rem if it is the target line then write it
if not %%i == !var! (
set /p other=
rem echo writing !other!
echo !other!>> %1.temp
)
)
rem now write the target line at the end
rem echo appending !found!
echo !found!>> %1.temp
)
rem replace the original with the temp version
move %1.temp %1>nul
)
rem print the result
type %1
Place in cmd file
for /f "tokens=2 delims=/" %%m in ('cmd /e:on /v:on /c "for /f %%f in (maclist.txt) do #echo !random!/%%f" ^| sort') do echo %%m
It spawns a cmd which reads the mac list in the inner for, prefixes a random value and a slash to the mac and sorts the list. Then this list is splitted in the outter for using the slash as delimiter and printing the mac address.

Slow processing a for loop that utilizes findstr

I've got a somewhat weird case, where a for-loop is incredibly slow when I use findstr as the string for DO.
Its worth mentioning that the file (old-file.xml) that I'm processing contains about 200 000 lines.
This part is blazing fast, but can be rendered slower if I remove | find /c ":"
rem find total number of lines in xml-file
findstr /n ^^ old-file.xml | find /c ":" > "temp-count.txt"
set /p lines=< "temp-count.txt"
The code which is slow looks like this and I can't use the pipe trick above. It seems like the slow part is the for itself, as i'm not seeing any progress in the title bar until after 10 min.
setlocal DisableDelayedExpansion
rem start replacing wrong dates with correct date
for /f "usebackq Tokens=1* Delims=:" %%i in (`"findstr /n ^^ old-file.xml"`) do (
rem cache the value of each line in a variable
set read-line=%%j
set line=%%i
rem restore delayed expansion
setlocal EnableDelayedExpansion
rem write progress in title bar
title Processing line: !line!/%lines%
rem remove trailing line number
rem set read-line=!read-line:*:=!
for /f "usebackq" %%i in ("%tmpfile%") do (
rem replace all wrong dates with correct dates
set read-line=!read-line:%%i=%correctdate%!
)
rem write results to new file
echo(!read-line!>>"Updated-file.xml"
rem end local
endlocal
)
EDIT:
Further investigation showed me that using this single line that should display the current line number being looped takes about 10 minutes on my 8MB file of 200 000 lines. That's just for getting it to start displaying the lines.
for /f "usebackq Tokens=1* Delims=:" %%i in (`"findstr /n ^^ old-file.xml"`) do echo %%i
So it seems like findstr is writing screen output hidden for the user, but visible for the for-loop. How can I prevent that from happening while still getting the same results?
EDIT 2: Solution
The solution as proposed by Aacini and finally revised by me.
This is a snippet from a much bigger script. Wrong dates are retrieved in another loop. And total number of lines are also retrieved from another loop.
setlocal enabledelayedexpansion
rem this part is for snippet only, dates are generated from another loop in final script
echo 2069-04-29 > dates-tmp.txt
echo 2069-04-30 >> dates-tmp.txt
findstr /n ^^ Super-Large-File.xml > out.tmp
set tmpfile=dates-tmp.txt
set correctdate=2011-11-25
set wrong-dates=
rem hardcoded total number of lines
set lines=186442
for /F %%i in (%tmpfile%) do (
set wrong-dates=!wrong-dates! %%i
)
rem process each line in out.tmp and loop them through :ProcessLines
call :ProcessLines < out.tmp
rem when finished with above call for each line in out.tmp, goto exit
goto ProcessLinesEnd
:ProcessLines
for /L %%l in (1,1,%lines%) do (
set /P read-line=
rem write progress in title bar
title Processing line: %%l/%lines%
for %%i in (%wrong-dates%) do (
rem replace all wrong dates with correct dates
set read-line=!read-line:%%i=%correctdate%!
)
rem write results to new file
echo(!read-line:*:=!>>"out2.tmp"
)
rem end here and continue below
goto :eof
:ProcessLinesEnd
echo this should not be printed until call has ended
:exit
exit /b
Two points here:
1- The setlocal EnableDelayedExpansion command is executed with every line of the file. This means that about 200000 times the complete environment must be copied to a new local memory area. This may cause several problems.
2- I suggest you to start with the most basic part. How much time takes the findstr to execute? Run findstr /n ^^ old-file.xml alone and check this before trying to fix any other part. If this process is fast, then add a single step to it and test again until you discover the cause of the slow down. I suggest you not use pipes nor for /f over the execution of findstr, but over the file generated by a previous redirection.
EDIT A faster solution
There is another way to do this. You may pipe findstr output into a Batch subroutine, so the lines can be read with SET /P command. This method allows to process the lines entirely via delayed expansions and not via the command-line susbtitution of FOR /F, so the pair of setlocal EnableDelayedExpansion and endlocal commands are no longer necessary. However, if you still want to display the line number it is necessary to calculate it again.
Also, it is faster to load the wrong dates in a variable instead of process the %tmpfile% with every line of the big file.
setlocal EnableDelayedExpansion
rem load wrong dates from tmpfile
set wrong-dates=
for /F %%i in (%tmpfile%) do (
set wrong-dates=!wrong-dates! %%i
)
echo creating findstr output, please wait...
findstr /n ^^ old-file.xml > findstr.txt
echo :EOF>> findstr.txt
rem start replacing wrong dates with correct date
call :ProcessLines < findstr.txt
goto :eof
.
:ProcessLines
set line=0
:read-next-line
set /P read-line=
rem check if the input file ends
if !read-line! == :EOF goto :eof
rem write progress in title bar
set /A line+=1
title Processing line: %line%/%lines%
for %%i in (%wrong-dates%) do (
rem replace all wrong dates with correct dates
set read-line=!read-line:%%i=%correctdate%!
)
rem write results to new file
echo(!read-line:*:=!>>"Updated-file.xml"
rem go back for next line
goto read-next-line
SECOND EDIT An even faster modification
Previous method may be slighlty speeded up if the loop is achieved via for /L command instead of via a goto.
:ProcessLines
for /L %%l in (1,1,%lines%) do (
set /P read-line=
rem write progress in title bar
title Processing line: %%l/%lines%
for %%i in (%wrong-dates%) do (
rem replace all wrong dates with correct dates
set read-line=!read-line:%%i=%correctdate%!
)
rem write results to new file
echo(!read-line:*:=!>>"Updated-file.xml"
)
This modification also omit the :EOF comparison and the calculation of line number, so the time gain may be significative after repeated it 200000 times. If you use this method, don't forget to delete the echo :EOF>> findstr.txt line in first part.
A FOR /F expression will always executed/read/evaluated complete before the inner loop starts.
You can try it with
(
echo line1
echo line2
) > myFile.txt
FOR /F "delims=" %%a in (myFile.txt) DO (
echo %%a
del myFile.txt 2> nul >nul
)
It will display
line1
line2
In your case the complete ('"findstr /n ^^ old-file.xml"') will executed and cached before the loop can start
EDIT: Added Solution
I measured with a file ~20MB with 370.000 lines
type testFile.txt > nul
findstr /n ^^ testFile.txt > nul
for /F "delims=" %%a in (testFile.txt) do (
rem Nothing
)
for /f "usebackq delims=" %%a in (`"findstr /n ^^ testFile.txt"`) do ...
findstr /n ^^ testFile.txt > out.tmp
type_nul ~10000ms
findstr_nul ~30000ms
for_file ~ 1600ms
for_findstr cancled after 10 minutes
findstr_tmp ~ 500ms !!!
I would recommend to use a temporary file, it's extremly fast.
findstr /n ^^ myFile.txt > out.tmp
set lineNr=0
(
for /f "usebackq delims=" %%a in ("out.tmp") do (
set /a lineNr+=1
set "num_line=%%a"
setlocal EnableDelayedExpansion
set "line=!num_line:*:=!"
echo(!line!
endlocal
)
) > out2.tmp
Btw. Your for/F splitting can fail, if the original line begins with a colon
for /f "usebackq Tokens=1* Delims=:"
Sample: :ThisIsALabel
:ThisIsALabel
Findstr /n prepends a line number
17::ThisIsALabel
the delims=: will split the first token and handles all colons as only one seperator
ThisIsALabel

Resources