How to do an while loop in batch through command line args - windows

I'm trying to run a batch script through the command line arguments to avoid writing a batch script to file every time I need it.
The sample batch script is
:loop
timeout /t 1
goto loop
I pass it to cmd.exe with the command line
cmd.exe /C ":loop & timeout /t 1 & goto loop"
It works for commands like timeout, but it doesn't work if I include labels such as loop.
It simply exits without saying anything.
Is it possible to do labels and if else statements through the /C command in batch?

You can't use labels on the command line, but if you're looking for an infinite loop, it's possible to abuse the for /L command so that the counter never reaches its target.
for /L %A in (,,) do timeout /t 1
Note that if you want to run this in a script, you need to change %A to %%A because of how escaping %s works.

Related

Waiting for Process to terminate on a remote PC using for loop

Should be able to sort this but I'm going round in circles. I know this has to do with setlocal
EnableDelayedExpansion, but I'm missing something.
Goal:
Execute a windows (cleanmgr.exe) script on a remote machine, wait till Cleanmgr.exe closes then have
the initiating script "type" the resultant log file (generated via cleanup script) from the remote
system in the CMD window.
What's working:
The script running on the remote machine runs fine, it echo's C: free drive space into a log file,
then cleans up the PC, and then re runs the disk space report and echo's result into same log file,
so the user can see (/have transparency of) the reclaimed space via the before & after results.
What's Broken:
The WMIC command to check for Cleanmgr.exe on the target PC, only works once, when it waits to retry
the variable containing the Hostname has been wiped out. I can see the behavior by echoing the
variable back.
Fix Attempts:
I have a hunch this has to do with the variable being lost once the if statement is ran within the
Parentheses. I have tried lots of options but they all behave the same. I have tried jumping the
process out to loop outside the original code using %1 instead of %%i but just cant quite get there.
Thanks for any improvements.
#echo off
pushd %~dp0
color 1e
setlocal EnableDelayedExpansion
title HDD Space Checker...
for /f %%i in (hostnames.txt) do (
xcopy /y cleanupwindows-sfd.bat \\%%i\C$\IT
WMIC /node:"%%i" process call create "C:\IT\cleanupwindows-sfd.bat"
echo Waiting For Processes...
timeout -t 10 /nobreak >nul
:loop
WMIC /node:"%%i" process where name="cleanmgr.exe" get name |find "cleanmgr.exe">nul
IF "!errorlevel!"=="0" set running=var
IF "!running!"=="var" timeout -t 4 >nul & echo Still Running & goto :loop
IF "!running!"=="" timeout -t 4 >nul & type \\%%i\C$\IT\%%i_HHD_Space.log
)
pause
exit
There is at least two points to see.
Your running variable, once set, is never reset, triggering an infinite loop
Your goto statement inside the enclosing parenthesis drives the command interpreter (cmd.exe) to stop evaluating the block, thus your script loose the %%i and leave the for loop, thus when terminating the :loop cycle your script will leave the for loop without cycling to other values in hostnames.txt.
To address that, put your process code in a subroutine called with CALL and reset the running variable at each :loop cycle :
#echo off
pushd %~dp0
color 1e
setlocal EnableDelayedExpansion
title HDD Space Checker...
for /f %%i in (hostnames.txt) do (
CALL:Process "%%i"
)
pause
exit
:Process
xcopy /y cleanupwindows-sfd.bat \\%~1\C$\IT
WMIC /node:"%~1" process call create "C:\IT\cleanupwindows-sfd.bat"
echo Waiting For Processes...
timeout -t 10 /nobreak >nul
:loop
set "running="
WMIC /node:"%~1" process where name="cleanmgr.exe" get name |find "cleanmgr.exe">nul
IF "!errorlevel!"=="0" set "running=var"
IF "!running!"=="var" timeout -t 4 >nul & echo Still Running & goto :loop
IF "!running!"=="" timeout -t 4 >nul & type \\%~1\C$\IT\%~1_HHD_Space.log
GOTO:EOF
Explanations: The CALL statement implies that the command interpreter will store the current executed line of your script and its state before executing the associated subprocess/command/etc.. When the subprocess/command/etc.. finishes, the command interpreter resumes its execution of the script to the next line with a restored context. This avoids then the loose of the for loop context.

Execute multiple batch files concurrently and monitor if their process is completed

I have a main batch file which calls multiple batch files. I want to be able to execute all these batch files at the same time. Once they are all done, I have further processes that needs to carry on in the main batch file.
When I use 'Start' to call the multiple batch files, I'm able to kick off all batch files concurrently but I lose tracking of them. (Main batch file thinks their processes are done the moment it executes other batch files).
When I use 'Call', I'm able to monitor the batch file process, but it kicks off the batch files sequentially instead of concurrently.
Is there a way around this? I have limited permissions on this PC and I'm trying to accomplish this using Batch only.
Main Batch file
call first.bat
call second.bat
call third.bat
:: echo only after all batch process done
echo done!
first.bat
timeout /t 10
second.bat
timeout /t 10
third.bat
timeout /t 10
This is the simplest and most efficient way to solve this problem:
(
start first.bat
start second.bat
start third.bat
) | pause
echo done!
In this method the waiting state in the main file is event driven, so it does not consume any CPU time. The pause command would terminate when anyone of the commands in the ( block ) outputs a character, but start commands don't show any output in this cmd.exe. In this way, pause keeps waiting for a char until all processes started by start commands ends. At that point the pipe line associated to the ( block ) is closed, so the pause Stdin is closed and the command is terminated by cmd.exe.
This will generate a temporary file and lock it by creating a redirection to it, starting the batch subprocesses inside this redirection. When all the subprocesses end the redirection is closed and the temporary file is deleted.
While the subprocesses are running, the file is locked, and we can test this trying to rename the file. If we can rename the file, subprocesses have ended, else some of the processes are still running.
#echo off
setlocal enableextensions disabledelayedexpansion
for %%t in ("%temp%\%~nx0.%random%%random%%random%.tmp") do (
echo Starting subprocesses
9> "%%~ft" (
start "" cmd /c subprocess.bat
start "" cmd /c subprocess.bat
start "" cmd /c subprocess.bat
start "" cmd /c subprocess.bat
start "" cmd /c subprocess.bat
)
echo Waiting for subprocesses to end
break | >nul 2>nul (
for /l %%a in (0) do #(ren "%%~ft" "%%~nxt" && exit || ping -n 2 "")
)
echo Done
) & del "%%~ft"
note: any process started inside the subprocesses will also hold the redirection and the lock. If your code leaves something running, this can not be used.
#ECHO Off
SETLOCAL
:: set batchnames to run
SET "batches=first second third"
:: make a tempdir
:maketemp
SET /a tempnum=%random%
SET "tempdir=%temp%\%tempnum%"
IF EXIST "%tempdir%*" (GOTO maketemp) ELSE (MD "%tempdir%")
FOR %%a IN (%batches%) DO START "%%a" %%a "%tempdir%\%%a"
:wait
timeout /t 1 >nul
FOR %%a IN (%batches%) DO IF exist "%tempdir%\%%a" GOTO wait
RD "%tempdir%" /S /Q
GOTO :EOF
Where the batches are constructed like
#ECHO OFF
:: just delay for 5..14 seconds after creating a file "%1", then delete it and exit
SETLOCAL
ECHO.>"%~1"
SET /a timeout=5+(%RANDOM% %% 10)
timeout /t %timeout% >NUL
DEL /F /Q "%~1"
EXIT
That is, each called batch first creates a file in the temporary directory, then deletes it after the required process is run. The filename to create/delete is provided as the first parameter to the batch and "quoted" because the temp directoryname typically contains separators.
The mainline simply creates a temporary directory and invokes the subprocedures, then repeatedly waits 1 second and checks whether the subprocedures' flagfile have all been deleted. Only if they have all been deleted with the procedure continue to delete the temporary directory
Adding to the answer by Aacini. I was also looking for similar task. Objective was to run multiple commands parallel and extract output (stdout & error) of all parallel processes. Then wait for all parallel processes to finish and execute another command. Following is a sample code for BAT file, can be executed in CMD:
(
start "" /B cmd /c ping localhost -n 6 ^>nul
timeout /t 5 /nobreak
start "" /B /D "C:\users\username\Desktop" cmd /c dir ^> dr.txt ^2^>^&^1
start "" /B cmd /c ping localhost -n 11 ^>nul
timeout /t 10 /nobreak
) | pause
Echo waited
timeout /t 12 /nobreak
All the statements inside () are executed first, wait for them to complete, then last two lines are executed. All commands begining with start are executed simultaneously.

How to catch only the last line of another command executed inside batch script with CALL

I have to write a simple batch script for windows that have to "launch" another script and return only the last line of the output of the launched program.
I need this to catch the "status" of the launched command, that it's printing something like: "End programm with code 0" after many ( and not known number of other line )
What I need is to catch only the last line of myprogram.cmd inside my catch.cmd
I'll try to use CALL inside my catch.cmd script to run myprogram.cmd, but I don't know how to manipulate and get back the last output line.
If I use only CALL myprogram.cmd I'll get all the output inside my catch.cmd, I'll try with CALL myprogram.cmd >NULL to hide all the output inside my catch.cmd, but how to get the last line of the myprogram.cmd?
Thanks for any suggestion
I've created two files.
called.bat:
#echo off
dir c:\
caller.bat:
#echo off
for /f "tokens=*" %%f in ('call called.bat') do set lastLine=%%f
echo %lastLine%
Running caller.bat will execute called.bat "in silence" and save the last line in the variable %lastLine%.
So in your case the anser will be this line:
...
for /f "tokens=*" %%f in ('call myprogram.cmd') do set lastLine=%%f
if "%lastLine%"=="End programm with code 0" (
::DO SOMETHING
)
...

How to signal one always running command process to stop from another command process?

How would I go about having one command process send a command to another one?
Well, specifically a batch script. I have one that starts a service, and runs it 24 hours per day and 7 days per week in a command prompt window.
What I need is a batch script to send a command "stop" to the other command process.
I have looked this up online, but there's nothing too specific about this, and doesn't accomplish what I need.
If the intent is to stop the other command prompt, why not just kill it?
Taskkill could do this for you. For Eg:
Taskkill /PID 2704 /F
It is perhaps possible to use a file as indication for the nearly always running command process to terminate itself.
Example code for nearly always running batch file AlwaysRunning.bat:
#echo off
rem The example code in a nearly endless running loop just prints
rem a message and then waits 3 seconds to simulate process time
rem of other commands. Then the batch file checks if the file
rem used to signal a wanted exit of this batch file exists.
rem The file is deleted and command process exits in this case.
title %~n0
:Endless
echo %TIME% %~0 is running.
%SystemRoot%\System32\ping.exe 127.0.0.1 -n 4 >nul
if exist "%TEMP%\ExitAlwaysRunning.tmp" (
rem Other commands to execute before exit.
del "%TEMP%\ExitAlwaysRunning.tmp"
exit
)
goto Endless
The code for the other batch file ExitRunning.bat signaling with file ExitAlwaysRunning.tmp in directory for temporary files the batch file AlwaysRunning.bat to exit as soon as possible.
#echo off
rem Create in folder for temporary files an empty file as
rem indication for the always running batch task to exit.
copy NUL "%TEMP%\ExitAlwaysRunning.tmp" >nul
rem Define a maximum time of 10 seconds waiting for exit
rem of the other command process signaled by file just
rem created not existing anymore because deleted by the
rem nearly always running command process immediately
rem before exiting.
set "Timeout=10"
rem Check once per second if the other command process
rem deleted the temporary file immediately before exiting.
:WaitForExit
%SystemRoot%\System32\ping.exe 127.0.0.1 -n 2 >nul
if not exist "%TEMP%\ExitAlwaysRunning.tmp" (
echo.
echo AlwaysRunning.bat deleted file and exited.
goto ProcessExited
)
set /A Timeout-=1
if not %Timeout% == 0 (
echo %Timeout% sec remaining on waiting for exit.
goto WaitForExit
)
del "%TEMP%\ExitAlwaysRunning.tmp"
echo.
echo Command process processing AlwaysRunning.bat is either
echo not running or ignored the indication by file to exit.
rem Command tasklist could be used to find out if the other batch file
rem is still running and then taskkill could be used to kill the other
rem command process when not exiting within specified time. But this
rem should not happen if the initial timeout value is set correct.
:ProcessExited
rem Put here your other commands executed after nearly
rem always running command process terminated itself.
echo.
pause
Double click or run from within a command prompt window first AlwaysRunning.bat.
Then start in another command prompt window or with a double click ExitRunning.bat.

How to make windows batch file pause when double-clicked?

I've written a batch file to automate some tasks. I can run it from a command window and it runs and displays results. If I double click it from explorer though, it runs and terminates immediately so I can't see the results.
Is there a way I can make batch file window stay open until I dismiss it if I started it by double-clicking the icon?
I don't want to have to pass a /nopause parameter or something when I call a batch file from the command line. I'd like a solution where I can use the batch file without having to do anything too special?
Thanks.
NOTE I don't want it to pause when running from the command line!! I may call this batch file from another batch file to carry out a load of operations. In that case I can't be sitting there to keep hitting enter.
Ideally it would be best if I can put some code in the batch file so it can work out where it was started from, and then pause or not as appropriate.
Use:
cmd /K myBatch.bat
as your shortcut.
My problem was similar - I wanted a pause at the very end if called from Windows Explorer, but no pause if in a command window. I came up with this.
At the top of every batch file, put this:
if "%parent%"=="" set parent=%~0
if "%console_mode%"=="" (set console_mode=1& for %%x in (%cmdcmdline%) do if /i "%%~x"=="/c" set console_mode=0)
and then at end
if "%parent%"=="%~0" ( if "%console_mode%"=="0" pause )
It handles nested batch calls, where you only want to pause at the end of the original batch file, not in nested batch files. In a nested batch file, %parent% is already set to original caller so it won't equal the nested %~0. If you have bat1 which calls bat2, it leaves open the option of double clicking bat2 in Explorer - in that context bat2 will pause at end, whereas if bat1 calls bat2, then bat2 won't pause at the end (only bat1 will pause).
The & statement separator helps avoid visual clutter for something which is secondary to the main function. If you don't like the look of it, just put each statement on a new line.
This approach looks for /C as one of the params in the launch command (%cmdcmdline%). It assumes your batch files don't use the /C option. If you use /C yourself, then you need to instead check if %COMSPEC%appears within %cmdcmdline% (use FINDSTR). When Explorer launches a bat file, its %cmdcmdline% includes %COMSPEC% eg C:\Windows\System32\cmd.exe /C double_clicked_batch_file_name. In a command window, %cmdcmdline% just has cmd.exe in it (no path). I use CALL mybat rather than cmd.exe /C mybat, but you may have to work with existing batch files.
Here is a solution that should work well and take into consideration the possibility that a batch-file might call another batch-file ("Nested").
You could use Find, to look for "/c" which should not be present if the batch-file is run from a "Command Prompt":
echo %cmdcmdline% | find /i "/c"
But, you could do a more "robust" test by using Find to search for a longer string, or the batch-file name.
The "Find" command will not work properly if the search string has (") double-quotes within it. To work around that, you can use environment variable substitution to "adjust" the string so it plays nice with Find:
set newcmdcmdline=%cmdcmdline:"=-%
This will typically return:
if the batch-file is run from a "Command Prompt"
newcmdcmdline=-C:\Windows\system32\cmd.exe-
if the batch-file is run by clicking on the the batch-file
(or the batch-file shortcut) from "Windows Explorer"
newcmdcmdline=cmd /c --D:\Path\test.cmd- -
Then you can use "Find" to test like:
echo %newcmdcmdline% | find /i "cmd /c --"
or
echo %newcmdcmdline% | find /i "cmd /c --%~dpf0%-"
Next, you need to decide if you want a "Nested" batch-file to behave as if you executed it in the same way as the calling batch-file, or if you want nested batch-files to always behave as if they were executed from a "Command Prompt".
Consider that if you are running in a nested batch-file, then testing for this:
echo %newcmdcmdline% | find /i "cmd /c --%~dpf0%-"
will always fail (no match) because %newcmdcmdline% contains the name of the outermost batch-file, not the nested batch-file.
So the first solution will behave the same for the calling batch-file, and all nested batch-files. Also perfect if you don't call any batch-files:
In all batch-files (calling and nested) that you care to make this test, add these lines, typically near the top of the batch-files (you may exclude the echo-statements if you wish):
if not defined withincmd call :testshell
if %withincmd% EQU 0 echo This batch-file: %~dpf0 was executed directly (from Windows Explorer, ...).
if %withincmd% EQU 1 echo This batch-file: %~dpf0 was executed from within a Command Prompt
rem if %withincmd% EQU 0 pause
Then, somewhere within each batch-file, add the testshell sub-function:
goto :EOF
:testshell
rem "Nested" batch-files won't see this because withincmd is already defined
if not defined newcmdcmdline set newcmdcmdline=%cmdcmdline:"=-%
set withincmd=1
echo %newcmdcmdline% | find /i "cmd /c --%~dpf0%-"
if %errorlevel% EQU 0 set withincmd=0
goto :EOF
You only make the conditional call to "testshell" one time, at the top of the outermost batch-file.
In some situations, you may want to have only the "outermost" batch-file behave differently if it is executed from a "Command Prompt" versus if it is run by clicking on the the batch-file (or the batch-file shortcut) from "Windows Explorer". So, batch-files called from the "outermost" batch-file will always behave the same regardless of how they are run.
For this to work, you have a few choices.
1) Save the value of "withincmd" before you call another batch-file, and restore the previous value of "withincmd" after the called batch-file returns. This is a little involved for most cases.
2) Use a "globally-unique" variable name for "withincmd" in each batch-file.
3) Execute the "Find" command each time you need to know how the current batch-file was run.
4) Increment a variable on entry to a batch-file and decrement it on batch-file exit, then only test how batch-file was run if count-variable=1
Method 3 is the easiest, but has the downside that if the outermost batch-file is called from itself (as in recursion) or another batch-file, the test variable (withincmd) will not be properly set.
Here's how to do it using method 3:
In all batch-files (calling and nested) that you care to make this test, add these lines, typically near the top of the batch-files (you may exclude the echo-statements if you wish):
call :testshell
if %withincmd% EQU 0 echo This batch-file: %~dpf0 was executed directly (from Windows Explorer, ...).
if %withincmd% EQU 1 echo This batch-file: %~dpf0 was executed from (or Nested) within a Command Prompt
rem if %withincmd% EQU 0 pause
Then, somewhere within each batch-file, add the testshell sub-function:
goto :EOF
:testshell
if not defined newcmdcmdline set newcmdcmdline=%cmdcmdline:"=-%
set withincmd=1
echo %newcmdcmdline% | find /i "cmd /c --%~dpf0%-"
if %errorlevel% EQU 0 set withincmd=0
goto :EOF
In this case, you have to call "testshell" once, at the top of the EACH batch-file, then again after you have returned from calling another batch-file (or call "testshell" each time you need to know how the current batch-file was run).
Here's how to do it using method 4:
In all batch-files (calling and nested) that you care to make this test, add these lines, typically near the top of the batch-files (you may exclude the echo-statements if you wish):
if not defined nestinglevel set nestinglevel=0
set /A nestinglevel=nestinglevel+1
call :testshell
if %withincmd% EQU 0 echo This batch-file: %~dpf0 was executed directly (from Windows Explorer, ...).
if %withincmd% EQU 1 echo This batch-file: %~dpf0 was executed from (or Nested) within a Command Prompt
rem if %withincmd% EQU 0 pause
Then, somewhere within each batch-file, add the testshell sub-function:
goto :EOF
:testshell
if not defined newcmdcmdline set newcmdcmdline=%cmdcmdline:"=-%
set withincmd=1
if %nestinglevel% GEQ 2 goto :EOF
echo %newcmdcmdline% | find /i "cmd /c --%~dpf0%-"
if %errorlevel% EQU 0 set withincmd=0
goto :EOF
Also, remember to decrement the variable when you exit one batch-file to return to the calling batch-file:
set /A nestinglevel=nestinglevel-1
In this case, you have to call "testshell" once, at the top of the EACH batch-file, then again after you have returned from calling another batch-file (or call "testshell" each time you need to know how the current batch-file was run).
In all cases, test %withincmd% to determine how the current batch-file was run, like this:
if %withincmd% EQU 0 pause
if %withincmd% EQU 1 goto :EOF
at the end of file print
pause
it will wait for anykey input
Add this at the end of your batch:
echo %CMDCMDLINE% | findstr /C:"/c">nul && pause
This will pause if run from Windows Explorer and do nothing if run from command line.
Explanation:
CMDCMDLINE contains "/c" when run from Windows Explorer.
echo %CMDCMDLINE% | will pipe contents of the CMDCMDLINE into findstr
findstr /C:"/c" checks if CMDCMDLINE contains "/c"
">nul" will discard findstr console output
&& pause will run only if findstr finds something
you can just add params to the batch call and handle conditionally pause statement in your batch. So when started from command line or dblclick the batch can pause, when called from others batches with a /nopause param don't pause.
use "pause" in the batch file at the end, and it will wait for the user input
HTH
Would the pause command work?
Microsoft Documentation on pause

Resources