How to pass an error message occuring inside the FOR /F loop body into the FOR parameter in Batch - batch-file

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

Related

Batch: How to store command output with spaces in variable?

I want to make a Batch file to create windows firewall rules for an executable (for example, python). To do this, I have the following script:
REM ~ Open ports. Run with elevated administrator rights.
CALL :OPEN "python"
GOTO :EOF
:OPEN
IF ERRORLEVEL 1 (
GOTO :EOF
)
FOR /F %%i IN ('where %1') DO CALL :OPEN_PROGRAM "%1" %%i && GOTO :EOF
GOTO :EOF
:OPEN_PROGRAM
FOR %%j IN (TCP UDP) DO CALL :OPEN_PROGRAM_PORT %1 %2 "%%j"
GOTO :EOF
:OPEN_PROGRAM_PORT
netsh advfirewall firewall add rule name=%1 description=%1 dir=in action=allow edge=deferuser profile=private program=%2 protocol=%3
GOTO :EOF
This works fine as long as there are no spaces in the path to the executable.
If there are spaces in the path, then only the part up to the spaces is put in the %%i variable. I tried using 'where /F %1' , but then it still cuts the output on the space.
Is there a way to store the full output of where %1 in a variable? (Preferably without writing it to a file)
I would first advise you not to pass an filename without an extension. Doing so relies upon each of the extensions listed under %PATHEXT%, and you could have files named python.com, python.bat, python.cmd, python.vbs, python.vbe, python.js, python.jse, python.wsf, python.wsh, and python.msc, as well as python.exe. I'm sure you wouldn't want to unknowingly create firewall rules for all of those.
I would also advise that you do not use the where command for this. The reason being that it could return more than a single item. For example, try this, where notepad.exe and if memory serves, you'll probably see, C:\WINDOWS\System32\notepad.exe, followed by C:\WINDOWS\notepad.exe. That is because where.exe searches each location in %PATH% in order of their listing, and returns each, as the first two entries under %PATH% should by default be C:\WINDOWS\system32;C:\WINDOWS;.
My suggestion therefore would be to use another method, which returns the first item found under %PATH% as opposed to all, and this should, if you've used the variable properly, return the default, (most used) item. The method is the %~$PATH variable expansion, and is documented under the help information for the for command, i.e. entering for /? at the Command Prompt.
Example Script:
#Echo Off
SetLocal EnableExtensions
Rem ~ Open ports. Run with elevated administrator rights.
"%__AppDir__%reg.exe" Query HKU\S-1-5-19 1>NUL 2>&1 || (
Echo This script must be run elevated.
Echo Please use 'Run as administrator'
"%__AppDir__%timeout.exe" /T 3 /NoBreak 1>NUL
GoTo :EOF)
Call :OpenPorts "python.exe"
Pause
GoTo :EOF
:OpenPorts
For %%G In ("%~1") Do For %%H In (TCP UDP) Do (
"%__AppDir__%netsh.exe" AdvFirewall Firewall Add Rule^
Name="%~n1" Description="%~1" Dir=in Action=allow^
Edge=deferuser Profile=private Program="%%~$Path:G"^
Protocol=%%H)
Exit /B
I have removed all of the unnecessary Call commands, (using just the one), and deliberately used carets to split up your super long line, for readability; (which also allows you to include the localport= and/or remoteport= option, which I'd assume by your 'Open Ports' name you're going to want to include as a modification to the above later). I also took the liberty of including some code, to determine if the script was being run elevated, and display a message before closing, if it isn't.
Write the output of where.exe to a file, then read the file one line at a time. There is a way to do it without a temp file after the "===" line.
#ECHO OFF
SET "XTOFIND=%~1"
SET "TEMPFILE=%TEMP%\wherelist.tmp"
IF EXIST "%TEMPFILE%" (DEL "%TEMPFILE%")
"%__appdir__%where.exe" "%XTOFIND%" >"%TEMPFILE%"
FOR /F "tokens=*" %%A IN ('TYPE "%TEMPFILE%"') DO (
ECHO CALL :OPEN_PROGRAM "%%~A" %2 "%%J"
)
IF EXIST "%TEMPFILE%" (DEL "%TEMPFILE%")
ECHO ============
SET "XTOFIND=%~1"
FOR /F "tokens=*" %%A IN ('"%__appdir__%where.exe" "%XTOFIND%"') DO (
ECHO %%~A
ECHO CALL :OPEN_PROGRAM "%%~A" %2 "%%J"
)
EXIT /B 0

Native cmd tail command for windows

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.

Redirecting default printer (which is unknown) to LPT3

I'm trying to use the same script "prnmngr.vbs" from Microsoft to redirect the default printer to the LP3 port. I guess this is the most effective and standard way to detect default printers that I found so far but no success so far. I would like to keep it really simple using a .cmd file.
The code below will map the default printer using "WMIC" but it will not redirect to any LPT:
#echo off
FOR /F "tokens=2* delims==" %%A in (
'wmic printer where "default=True" get name /value'
) do SET DefaultPrinter=%%A
exit
My goal as I said is to Redirect the unknown default printer to the LP3 on the unknown computer and it will be great if the result from prnmngr.vbs can be parsed somehow in order to map the Default Printer to the LP3 port. Or to modify the prnmngr.vbs to do the usual command :
net use lpt3 \\computername\sharedprinter
So far I just can't handle the whole VBS coding to do what I need. I got confused with all the functions inside.
The command below will map the printer without any problems only if you have both \Servername and \Sharedprinter but on my case they will be unknown.
Cscript C:\Windows\System32\Printing_Admin_Scripts\en-US\prnmngr.vbs -ac -p "\\servername\sharedprinter"
The Default printer could be a shared network printer or a USB/WiFi and obviously the computer name will also be unknown. I can't check one by one between 5000 machines or users to search and get their devices to give them a command to map their printers on their own every time they execute their application!
I just been told that the name "Imprimant" is the name all the Local Printers shared name will be used so as long as this name doesn't change at all it should be fine.
But at the moment this script is working only for the 1st case (:Shared) and the Local case is not working at all. If I invert the ERRORLEVEL it will work the (:Local) and Not the (:Shared).
But so far I got a new scrip and it goes like this:
#echo off
FOR /F "tokens=2* delims==" %%A in ('wmic printer where "default=True" get name /value ' ) do (
setlocal EnableDelayedExpansion
set DefaultPrinter=%%A
echo %DefaultPrinter% |find "\\"
if ERRORLEVEL EQU 1 (goto Local
else (
if ERRORLEVEL EQU 0 goto Shared
)
:Shared
net use lpt3 "%DefaultPrinter%"
goto end
:Local
net use lpt3 "\\%computername%\Imprimant"
goto end
)
:end
exit /B
Is there something missing?
Voila, here you go !! The final script works fine.
#echo off
if not "%minimized%"=="" goto :minimized
set minimized=true
start /min cmd /C "%~dpnx0"
set minimized=
goto :EOF
:minimized
echo Erasing previous mappings from LTP3...
net use lpt3 /d >nul
echo Detecting Default Printer ...
FOR /F "tokens=1-3* skip=2 delims==" %%A in ('wmic printer where "default=True" get name^,servername^,sharename /value ' ) do (
setlocal EnableDelayedExpansion
set DefaultPrinter%%A=%%B
)
echo Default Printer: %DefaultPrinterName%
REM Is this a Local or Network queue?
echo %DefaultPrinterName% |find "\\" >nul
if %errorlevel%==0 ( goto Shared ) else ( goto Local )
:Shared
echo Mapping to LPT3 over %DefaultPrinterServerName%\%DefaultPrinterShareName%
net use lpt3 "%DefaultPrinterServerName%\%DefaultPrinterShareName%"
goto End
:Local
echo Using a Local Printer
IF NOT "%DefaultPrinterShareName%"=="" (
echo Mapping to LPT3 over \\localhost\%DefaultPrinterShareName%
net use lpt3 "\\localhost\%DefaultPrinterShareName%"
) ELSE (
echo Mapping to LPT3 over \\localhost\Impriman
net use lpt3 \\localhost\Impriman
)
:End
exit >> our you can attach other instructions to continue doing some tasks.
So this script will do what I meant to do.

Batch function not working correctly

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

For loop for checking the 'state' of certain windows' services

services.txt contains:-
Plugplay
spooler
dhcp
I want to check the status of some services that are specified in file services.txt. I am using for loop for this.
#echo off
for /f %%a IN ('type services.txt') do call :chkservice %%a
goto :eof
:chkservice
sc query %a%
Instead of getting the output for the three specified services, I am getting the output equivalent to three times the command sc query (I guess).
For debugging I tried checking if the variable a getting the values properly or not and tried this version of code:-
#echo off
for /f %%a IN ('type services.txt') do call :chkservice %%a
goto :eof
:chkservice
#echo on
echo %a%
This code display spooler and dhcp only. Why not plugplay? I Believe both the issues are related, but not sure how.
Any help on this would be highly appreciated.
If you call a function your parameters are in %1,%2,...%n not in %a%.
The parameters of a for-loop are nearly invisble outside of that loop.
So your code should looks like
#echo off
for /f %%a IN ('type services.txt') do call :chkservice %%a
goto :eof
:chkservice
echo %1
goto :eof

Resources