Read the first word on a specific line from a text file using batch script then feed into an if statement - batch-file

Sorry I know similar things have been asked here but basically I'm trying to read a text file within a batch script and evaluate what has been written to the file.
The job is a print job that sends a file to a printer, I have it echoing the output from the command to a log file. I then want to read in what the output was and if there was an error I will then send an email so we know when things stop working.
It always appends to the end of the file so I know if there's an error the 4th from last line will begin with "Error:". So my question is how can I read that in to a variable so I can perform an IF statement. I've got the emailing part sorted it's just reading from the file that I'm struggling with.
Any help would be much appreciated. Here's an example of the content of the file when there's an error:
----
C:\XG1\DGS01\prints\000000398200001.XG1
19/03/2013
15:02
1 file(s) copied.
Error: print server unreachable or specified printer does not exist.
1 file(s) moved.
It leaves one blank line at the end of the file so I'm going with the last line minus 4.
Thank you

as a line in your batch file:
for /f "tokens=1*delims=:" %%i in (thenameofyourfile) do if "%%i"=="Error" set message=%%j
echo message was "%message%"
Actually, that will report if ANY lines are in the format you describe.
#ECHO OFF
SETLOCAL
(SET message=)
FOR /f "tokens=1,2*delims=[]:" %%i IN (
' TYPE thenameofyourfile^|find /n /v "" '
) DO (
SET lastline=%%i
IF "%%j"=="Error" SET errorline=%%i&SET message=%%k
)
SET /a target=%errorline% + 3
IF %target% neq %lastline% (SET message=)
IF DEFINED message ECHO error found %message%
should get the line ONLY if it's the fourth last line in the file - the "+ 3" being the line-count required (well, minus 1)
BUT - remember that if this is, as it seems, a log file that it's possible (I'd imagine) that further entries may appear AFTER the error (for further jobs) so the target for the Line beginning "Error:" may not be the fourth-last...
OTOH, using the line(s) I first posted, once an "Error:..." line appears, it will be detected EVERY time - you'd need to reset the logfile in your mail-send procedure (save existing & recreate empty?)

Solution without any loop (does test if any error):
#findstr Error: printer.log >nul 2>&1
#if %errorlevel% equ 0 echo Send email now!
And this code does only test the fourth line before the last line for an error:
#echo off &setlocal enabledelayedexpansion
for /f %%i in ('findstr /n "^" printer.log') do (
set line4=!line3!&set line3=!line2!&set line2=!line1!&set line1=%%i)
echo %line4%|findstr Error: >nul 2>&1
if %errorlevel% equ 0 echo Send email now!

If you want to start reading the fourth line from the bottom of your log file, you can count the number of lines in your log file, subtract 4, then more +%count% to get the tail of the log.
#echo off
setlocal
set logfile=printerlog.log
for /f "tokens=2 delims=:" %%I in ('find /c /v "" "%logfile%"') do set lines=%%I
set /a "tail4=%lines% - 4"
for /f %%I in ('more +%tail4% "%logfile%" ^| find /i "Error:"') do (
rem do your email voodoo here
)
If your log file will exceed 65,535 lines, I recommend looping through the file using JScript or VBScript instead of a batch loop. Skipping that many lines with more will cause more to crash, so you'd have to loop line-by-line and increment a counter. However, batch for loops are terribly slow.
Here's the same script that replaces more with JScript-ish file reading.
#if (#a==#b) #end /*
:: batch portion
#echo off
setlocal
set logfile=printerlog.log
for /f "tokens=2 delims=:" %%I in ('find /c /v "" "%logfile%"') do set lines=%%I
set /a "tail4=%lines% - 4"
for /f "delims=" %%I in ('cscript /nologo /e:jscript "%~f0" %tail4% "%logfile%" ^| find /i "Error:"') do (
rem do your email voodoo here
rem %%I contains the matching line.
rem After one match, stop processing further
goto :EOF
)
goto :EOF
::JScript portion */
var i=0, fso = new ActiveXObject("Scripting.FileSystemObject").OpenTextFile(WSH.Arguments(1),1);
while (!fso.AtEndOfStream) {
if (i++ < WSH.Arguments(0)) fso.SkipLine();
else WSH.Echo(fso.ReadLine());
}
fso.Close();

I believe the fastest and least resource-intensive way to check for errors is with GNU tail. If you can, get the .zip binaries and put tail.exe in your path or where your batch script can access it. Then:
#echo off
setlocal
tail -n 4 printerlog.log | find /i "Error:" >NUL && (
echo Error found. Sending email.
rem do email stuff
)
Or if you wish to capture the text of the error for your email:
#echo off
setlocal
for /f "delims=" %%I in ('tail -n 4 printerlog.log ^| find /i "Error:"') do (
echo Error found: %%I
rem do email stuff
goto :EOF
)
tail is much more efficient than all these other methods of counting the number of lines in the log file and looping through the log file line-by-line, whether looping in batch or in JScript.

Related

batch pick random file that contains utf-8 characters in its name

I want to open a random file in a directory and its subdirectorys with batch. And I know there are enough questions on stackoverflow who give the code for that but none of which I found were with utf-8 character support.
I use the following code which I found in stackoverflow.
#echo off
setlocal
:: Create numbered list of files in a temporary file
set "tempFile=%temp%\%~nx0_fileList_%time::=.%.txt"
dir /b /s /a-d %1 | findstr /n "^" >"%tempFile%"
:: Count the files
for /f %%N in ('type "%tempFile%" ^| find /c /v ""') do set cnt=%%N
call :openRandomFile
:: Delete the temp file
del "%tempFile%"
exit /b
:openRandomFile
set /a "randomNum=(%random% %% cnt) + 1"
for /f "tokens=1* delims=:" %%A in (
'findstr "^%randomNum%:" "%tempFile%"'
) do start "" "%%B"
cmd /k
It works fine that way, until it picks a file like "blablabla_空色デイズ.mp3", in that case it gives an error like "file blablabla_?????.mp3 could not be found." and I have dozens of these files.
I have tried using chcp 65001 on the start of the file for using utf-8 and if I did so, the teporary created .txt list shows the correct names of japanese files, but the pick up itself does not work anymore after that, so I took away #echo off and cmd prints an error on set /a "randomNum=(%random% %% cnt) + 1": Error: division by zero.
And at this point, I dont understand anymore whats going on, because the file is working great without chcp 65001.
I don't know batch, please, does anyone have an idea how to make it run?
I would be really happy!
You will have a lot of problems with findstr and command output when utf8/unicode characters are involved. In this type of scenarios it is safer (but slower) to avoid them
#echo off
setlocal enableextensions disabledelayedexpansion
rem %1 = folder where to start to list files
rem if empty, current active directory is used
rem %2 = file mask
rem if empty * is used
rem What to search
set "fileMask=%~2" & if not defined fileMask set "fileMask=*"
rem Retrieve the number of matching files
set "nFiles=0" & for /r "%~f1." %%a in ("%fileMask%") do set /a "nFiles+=1"
if %nFiles% lss 1 exit /b
rem Select a random file
set /a "n=(%random% %% nFiles) + 1"
echo Selected file = %n% / %nFiles%
rem Count up to the selected file and start it
2>nul (
for /r "%~f1." %%a in ("%fileMask%") do (
set /a "n-=1", "1/n" || ( start "" "%%~fa" & goto :done )
)
)
:done
That is, count the number of files, select one of them and then start iterating over the list of files decrementing the number of the selected file. When it is 0 (we have reached the selected file), the 1/n division will fail and the conditional execution operator will execute the start command.
I'd try
) do start "" "%%~sB"
in the :openRandomFile procedure, which should open the file using its short name.
Sorry - can't test it as I don't have any UTF-8 filenames (that I know about)

Error ""tmp.txt" was unexpected at this time"

I am still pretty new to batch and im running into this issue that i cannot seem to solve. When I run my script it returns "6766.txt" was unexpected at this time.My script is supposed to search for "store_versions" in a file called Local State. If it finds that line it it will be appended to a Temp file and then trim the line in a loop so that i only get A specific part of the output of the find command. The output of the command is [319] "store_versions": { . And all I want is 319. Does anyone know what is causing this issue?
Thanks,
#echo off
setlocal enableextensions enabledelayedexpansion
set tempfile="%random%.txt"
set LineNumFin=%LineNumFin%
pause
Find /N "store_versions" < "Local State" > %tempfile%
for /f "tokens=1 delims=[]" %%a in %tempfile% do ( set LineNum=%%a
pause
)
del "%tempfile%"
echo Line Num: %LineNum%
set /a result=%LineNum%+4
echo %result%
The in clause of the for command needs parenthesis
for /f "... options ..." %%a in ( file ) do ....
^ ^
^ here ^
And, as you are including quotes in the name of the file, you will need to include usebackq in the options part of the for command
Or, you can avoid the temporary file
for /f "tokens=1 delims=[]" %%a in (
' find /n "store_versions" ^< "Local State" '
) do set "lineNum=%%a"
The for command will execute the command, tokenize the records and retrieve the value.

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.

Need to find text in log file and send following five lines to text file using .bat file

I've been searching for a few days and have been unable to find anything regarding my specific question.
I have a log file I would like to search (using findstr) for the words 'Login Successful", once this is found I would like a time stamp, that entire line and the next five lines sent to a text file.
I've played around with findstr and tried modifying other suggestions I've found here but have been unable to figure out a way to include those following five lines in my output.
Thank you in advance,
Eric
Download grep from UnxUtils
http://unxutils.sourceforge.net/
run:
grep -A 5 "Login Successful" [log file name] > output.txt
#echo off
setlocal EnableDelayedExpansion
rem Look for the line with "Login Successful" and output it
(for /F "tokens=1* delims=:" %%a in ('findstr /N "Login Successful" theFile.log') do (
set skip=%%a
echo %date% # %time%
echo %%b
)) > textFile.txt
rem Add the next five lines
set lines=0
for /F "skip=%skip% tokens=1* delims=:" %%a in ('findstr /N "^" theFile.log') do (
echo %%b>> textFile.txt
set /A lines+=1
if !lines! equ 5 goto :EOF
)
#ECHO OFF
SETLOCAL
SET /a gate=0
(
FOR /f "delims=" %%i IN ('type logfile.txt^|findstr /n "$" ') DO (
SET "line=%%i"
CALL :process
)
)>report.txt
GOTO :eof
:process
IF %gate% gtr 0 GOTO output
ECHO(%line%|FINDSTR /i /c:"login successful" >NUL
IF ERRORLEVEL 1 GOTO :EOF
SET /a gate=6
:output
SET /a gate -=1
SET "line=%line:*:=%"
ECHO(%line%
GOTO :eof
Since you don't say whether any of the 5 lines following may be empty, the findstr/n method is used to reproduce any empty line.
Essentially, if the target string is found (I've assumed case-insensitive) then open the gate to allow 6 lines through. If the gate is open, the leading line number is stripped and the required line echoed into the destination file.

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