I was working on a program of mine that echoes characters into files, but I ran into a problem.
Certain characters | < > & and some others cause an error when I echo them into the target file.
For example, if I have a script like:
if %char%==| echo ^|>> target.txt
When I run it, nothing happens. However, when I debug it, I get the error:
| was unexpected at this time.
Is there any way to detect those types of characters without it causing an error?
Thanks!
My code:
#echo off
empty> End.bat
for /f "tokens=*" %%a in (input.txt) do set id=%%a & call :processline %%a
exit
goto :eof
:processline
if %id:~0,1%==| <nul set /p=^|>> End.bat
echo.>> End.bat
goto :eof
:eof
Related
I am looking to build a tail to read log files on windows using only native cmd.
There are various ways to to read a file:
type file.txt
more file.txt
They however do not have a default option to read updates from a file, so does not represent anything like tail.
With a few little hacks and using more to read the file, skipping what was already read before, we are able to tail the file, "almost" realtime.
#echo off & set cnt=0
if "%~1" == "" echo no file specified, usage: "tail.cmd <filename>" & goto :eof
if not exist "%~1" echo file "%~1" does not exist & goto :eof
:tail_sub
2>nul (>>"%~1" echo off) && (goto :file) || (goto :tail_sub)
:file
for /f "tokens=1*delims=]" %%i in ('more "%~1" +%cnt% ^| find /v /n ""') do (
set "line=%%j"
set /a cnt+=1
call echo(%%line%%
)
goto :tail_sub
Currently, without adding a timeout to slow down the infinite loop, it consumes around 5.6MB memory, which is very acceptable in my view.
This does not yet take care of all special characters, like |<>&, but I will spend some time on this to cater for all scenarios I can think off.
I was wondering if I can capture error messages directly (just like an output from a successfully executed command would be) and do whatever I want with it, such as storing it in a variable, or passing it as a parameter.
Here's what I'm trying to do lately. Please check the code:
FOR /F "tokens=2 delims== " %%s IN (
'WMIC DISKDRIVE WHERE SerialNumber^="sn999" GET SerialNumber /VALUE'
) DO (
IF "%%s"=="sn999" (GOTO Label1) ELSE (GOTO Label2)
)
What I'm trying to do here is to check if a particular external hard drive is plugged-in by comparing the serial number, and from then, the code will determine which path to take. It works as intended if the desired external hard drive is plugged-in. However, if the external hard drive with the same serial number is not plugged-in, I get the following message:
No Instance(s) Available.
And as a consequence I get these issues:
ELSE clause would not push through
The code didn't raise the ERRORLEVEL. So, I can't use this either as a workaround.
My research led me to these sources:
Redirecting Error Messages from Command Prompt: STDERR/STDOUT
Display & Redirect Output
Both links discuss redirecting error messages. I thought I can just write few lines of code to redirect error messages to a text file, recover it and parse the contents, and finally clean it up. But I'm not sure if this is the best idea though. So, I'd appreciate any suggestion that you may have that may work with the code above. Also, I'd like to know why %ERRORLEVEL% is 0 even though I got an error message. Lastly, I'd like to know if it is possible to capture error messages without writing anything to the hard drive.
Thank you all very much!!
Put GOTO Label2 after the loop. If GOTO Label1 is triggered, then the GOTO Label2 will be avoided as it will jump to the label.
FOR /F "tokens=2 delims== " %%s IN (
'WMIC DISKDRIVE WHERE SerialNumber^="sn999" GET SerialNumber /VALUE'
) DO (
IF "%%~s"=="sn999" GOTO Label1
)
GOTO Label2
You could also suppress the stderr message of No Instance(s) Available. with 2^>nul if you prefer.
Using findstr makes you able to use errorlevel.
#echo off
for /F %%i in ('WMIC DISKDRIVE ^| findstr /I /C:"sn999"') do if %errorlevel%==0 echo Drive Found & goto :EOF
echo Drive not Found.
This basically does a search on the string and if found sets errorlevel to 0, if not found errorlevel will be 1.
If errorlevel is 0 then it will echo Drive Found and goto :EOF if errorlevel is not 1, it skips the line as the condition was not met and it will echo Drive not Found.
If you still want to use the labels, then simply do:
#echo off
for /F %%i in ('WMIC DISKDRIVE ^| findstr /I /C:"sn999"') do if %errorlevel%==0 goto :label1
REM This will act as label2
echo Drive not found
goto :EOF
:label1
echo Drive found
goto :EOF
Note we do not have to use goto label2 as it will automatically go passed the errorlevel match and continue, only label1 is really needed then.
I see no reason to use a For loop in this instance because a metavariable is not required.The For loop runs the WMIC command in another instance of cmd.exe unnecessarily.
#Echo Off
WMIC DiskDrive Where SerialNumber="sn999" List Instance 2>Nul|Find "I">Nul && GoTo :Label1
Echo Drive Not Found & Pause
GoTo :EOF
:Label1
Echo Drive Found & Pause
GoTo :EOF
I'm trying to make a mod loader for a game in batch but I can't get it to work with a nested loop.
The command used is this loadMods.bat mod1.txt mod2.txt ... modN.txt
This is the code I'm using
setlocal EnableDelayedExpansion
set /p outputFile="Output File:"
for %%x in (%*) do (
echo Applying mod from file %%x
for /f "delims=" %%y in (%%x) do echo %%y >> %fileOutput%
)
echo Finished.
pause
The second loop works fine if it's outside the first loop but when I use nested loops I get an The syntax of the command is incorrect. error on the second loop.
As #SomethingDark said, this is caused by a simple typographical issue.
for /f "delims=" %%y in (%%x) do echo %%y >> %fileOutput%
The variable fileoutput was undefined, making the CMD.EXE sees:
for /f "delims=" %%y in (%%x) do echo %%y >>
and it cannot find an argument after >>, causing the error.
By the way, when debugging batch files, I'd recommend:
remove #echo off
run script from CMD.exe
These method shows exactly which command(s) go wrong, making debugging easier.
As you appear only to be copying the contents of several files to a single output file you could probably simplify things a little too.
#Echo Off
If "%~1"=="" Exit/B
Set/P "outputFile= Output File: "
Type %* 2>Nul >"%outputFile%"
Echo( Finished.
Pause
Ensuring the validity of any passed arguments and input string is your responsibility.
You could even possibly bypass the input parameters:
#Echo Off
Set/P "outputFile= Output File: "
Type mod*.txt 2>Nul >"%outputFile%"
Echo( Finished.
Pause
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.
A while ago I made a function that you can call from the command prompt or any batch file (it was just for fun, I don't see how it could be useful). It basically just makes your (Microsoft) computer speak whatever you wrote in as the parameter.
I recently got some inspiration to add a switch to it where it would read the contents of a file. My standalone script worked, but when I added it to my function, it didn't work as I would have liked.
Here's the code:
#echo off & setlocal enabledelayedexpansion
if "%~1"=="/?" (
echo.
echo TALK "Text" [Parameters]
echo.
echo Text - The phrase you want to be spoken.
echo.
echo [Parameters]:
echo /f - Read the contents of a file. "Text" changes to the file path.
echo.
endlocal
exit /b
)
if "%~2 X" equ "/f X" (
if not exist %~1 (
echo File does not exist or cannot be found.
endlocal
exit /b
)
set cont=
for /f "delims=" %%i in (%~1) do set cont=!cont! %%i
:b
echo Set a = Wscript.CreateObject("SAPI.SpVoice") > "Talk.vbs"
echo a.speak "%cont%" >> "Talk.vbs"
start /WAIT Talk.vbs
del Talk.vbs
endlocal
exit /b
)
set text=%~1
echo set speech = Wscript.CreateObject("SAPI.spVoice") > "talk.vbs"
echo speech.speak "%text%" >> "talk.vbs"
start /WAIT talk.vbs
del Talk.vbs
endlocal
exit /b
Unfortunately I don't have working function code (before I added the /f switch).
This is a last resort for me as I've edited it heavily and scoured the code for any give away as to what the problem might be.
Another bad thing is that I didn't take note of what I changed, so I can't exactly tell you what I've tried. I can tell you what the outputs are though.
The first time I tried, it gave the output The syntax of the command is incorrect.
It's now at the point where the original function (just converting text to speech) doesn't work anymore. The contents of the file Talk.vbs (which was made during the process) is a.speak "".
I'll keep updating my attempts, but knowing me it's something really simple that I've overlooked.
--EDIT--
At the suggestion of someone, I put carats before the square brackets in the syntax section. Nothing changed.
Along with escaping the parenthesis you also had to surround if exist %~1 in quotes in case of a argument of "some words I want it to say". Also cleaned it up a bit. Code at the bottom, but first an explanation.
If you looked at talk.vbs before it was deleted you would see this:
a.speak "!cont! contents of the file here"
This is because of this code:
for /f "delims=" %%i in (%~1) do set cont=!cont! %%i
:b
echo Set a = Wscript.CreateObject("SAPI.SpVoice") > "Talk.vbs"
If you turned echo on and watched the code you would see the last unescaped ) was taking the contents of the for loop and including it in the redirect.
Corrected and cleaned code:
#echo off & setlocal enabledelayedexpansion
if "%~1"=="/?" (
echo.
echo TALK "Text" [Parameters]
echo.
echo Text - The phrase you want to be spoken.
echo.
echo [Parameters]:
echo /f - Read the contents of a file. "Text" changes to the file path.
echo.
endlocal
exit /b
)
set text=
if [%2]==[/f] (
if exist "%~1" (
for /f "usebackq delims=" %%i in (%1) do set text=!text! %%i
) else (
endlocal
exit /B
)
)
if [%2]==[] set text=%~1
echo set speech = Wscript.CreateObject^("SAPI.spVoice"^) > "talk.vbs"
echo speech.speak "%text%" >> "talk.vbs"
cscript //NoLogo //B talk.vbs
del Talk.vbs
endlocal
exit /b
Edit: fixed the for statement pointed out by Andriy M
In your echo statements that contain parentheses, try escaping the parentheses with carats. I suspect especially the echo within the if statement is partially getting evaluated literally.
One other minor suggestion, I would also replace
start /WAIT Talk.vbs
with
cscript /nologo Talk.vbs
It's not that I think the start /wait is causing the error, but it does cause a second console window to appear temporarily for no good reason -- or it will whenever your script executes that far, anyway.
I made a few other suggested changes here, such as eliminating the need for a /f switch. If "%1" is the name of a file that exists, read it. Otherwise, treat it as text to read. And instead of having a separate subroutine for reading a file versus getting text from input, all that needs to happen is a variable has a different value.
#echo off & setlocal enabledelayedexpansion
if "%1"=="/?" ( goto usage )
if "%1"=="" ( goto usage )
if "%1"=="--help" ( goto usage )
if exist "%1" (
set txt=
for /f "usebackq tokens=*" %%i in (%1) do set txt=!txt! %%i
) else (
set txt=%1
)
echo Set a = Wscript.CreateObject^("SAPI.SpVoice"^) > "talk.vbs"
echo a.speak "%txt%" >> "talk.vbs"
cscript /nologo talk.vbs
del talk.vbs
endlocal
goto :EOF
:usage
echo.
echo TALK ["text"^|filename]
echo.
echo talk filename -- speaks the contents of filename
echo talk "text" -- speaks the supplied text
endlocal
goto :EOF