How to traverse up directory structure using batch file - windows

I am writing a Windows batch script which compiles the files which are passed to it as arguments. Here is what I want to do:
Go to each file location.
Search the current folder for a 'makefile'.
If found, run 'make' and break out, else go to parent folder and repeat step-2.
Exit if root of current drive is reached.
Here is what I have been able to come up till now:
Input : List of full-paths to files which are to be compiled.
Example: "D:/dir1/dir2/file1.cxx" "D:/dir1/dir3/file2.cxx"
#echo off
REM -- loop over each argument --
for %%I IN (%*) DO (
cd %%~dpI
call :loop
echo "After subroutine"
)
exit /b
:loop
REM -- NOTE: Infinite loop, breaks out when root directory is reached --
REM -- or makefile is found --
for /L %%n in (1,0,10) do (
if exist "makefile" (
echo "Building.."
make -s
echo "Exiting inner loop"
exit /b 2
) else (
if "%cd:~3,1%"=="" (
echo "Reached root...exiting inner loop..."
exit /b 2
)
REM -- Go to parent directory --
cd ..
echo "Searching one level up"
)
)
Everything works file except this - After encountering the first 'makefile', the 'exit /b 2' causes the batch file to exit. What I want is that only the inner loop should exit. 'exit /b 2' is supposed to work according to this, but due to some reason it is not. Can anyone help me out with this?

There are a couple problems in your code. The less important one is that the comparison of the current directory in the inner loop must be done with delayed expansion. Now the important one:
There is no way to break a for /L loop with exit /B command. Although any command after the exit /B in the loop is not longer executed, the loop never ends. You must use plain exit command to do this, but of course the whole cmd.exe session is also terminated by exit, so the solution is to start a second cmd.exe session that re-execute the same Batch file controlled by a special parameter:
#echo off
REM If this batch file was re-executed from itself: goto right part
if "%~1" equ ":loop" goto loop
REM -- loop over each argument --
for %%I IN (%*) DO (
cd %%~dpI
REM Execute the "subroutine" in a separate cmd.exe session
cmd /C "%~F0" :loop
echo "After subroutine"
)
exit /b
:loop
setlocal EnableDelayedExpansion
REM -- NOTE: Infinite loop, breaks out when root directory is reached --
REM -- or makefile is found --
for /L %%n in () do (
if exist "makefile" (
echo "Building.."
make -s
echo "Exiting inner loop"
exit
) else (
if "!cd:~3,1!" equ "" (
echo "Reached root...exiting inner loop..."
exit
)
REM -- Go to parent directory --
cd ..
echo "Searching one level up"
)
)
EDIT: Couple comments added
When you use an infinite loop it is clearer to not include any value in the parentheses; otherwise it seems that you made a mistake in the "0" increment.
The description of EXIT command that appears at the link you gave is incorrect at this point...

Related

Check if file is locked inside a batch file for loop

I have a script that runs through some files and copies them to another location. But the script needs to wait until the file is no longer being written to.
I tried all the solutions here:
How to check in command-line if a given file or directory is locked (used by any process)?
Process a file after a file is finished being written Windows Command Line .bat
BATCH - wait for file to be complete before picking up
But the problem is that they don't work when wrapped in a loop. It always says the file is locked. If the script it cancelled and re-run it correctly finds the file unlocked.
Am I doing something wrong or is there a trick to make this work?
For locking a test file, checkfile.txt, I do:
(
>&2 pause
) >> checkfile.txt
Then the example script to check the file is this:
#echo off
for %%i in (*.txt) do (
:loop
ping localhost -n 5 > nul
echo "check if locked"
powershell -Command "$FileStream = [System.IO.File]::Open('%%i', 'Open', 'Write'); $FileStream.Close(); $FileStream.Dispose()" >NUL 2>NUL || (goto :loop)
echo "NOT locked anymore"
)
You cannot goto in a loop as it will simply break the for loop entirely. Additionally, the exit code or errorlevel is set for the last successful command. In this case being the powershell dispose command. Simply do the loop outside of the code block:
#echo off & setlocal
for %%i in (*.txt) do call :loop %%~i
goto :EOF
:loop
powershell -Command "[System.IO.File]::Open('%1', 'Open', 'Write')">nul 2>&1 && echo %1 not locked || (
echo %1 Locked
("%systemroot%\system32\timeout.exe" /t 3)>nul
goto :loop
)
Note, the conditional operators (and &&) and (or ||) helps to evaluate the exit code without needing to do if and else statements.

implicit "popd" on batch exit

Is there a way to undo all pushd at the end of script. What I have is:
pushd somwhere
rem doing stuff
goto end
:end
popd
goto :EOF
What I'd like to have is:
setlocal
pushd somwhere
rem doing stuff
goto :EOF
But that doesn't work, the directory stays "pushd". Another try where I can't say how many pushd will occur would be:
set CurrentDir=%CD%
rem doing a lot of pushd and popd
pushd somwhere
:POPBACK
if /i not "%CD%" == "%CurrentDir%" popd & goto POPBACK
But that looks like I can easily get stuck at :POPBACK. Adding a counter to exit that loop is a bit uncertain at which dir I end up.
In that context I'd like to know if a can see the stack of pushd directories. The only thing I found was $+ in prompt $P$+$G which adds a '+' for each pushd directory e.g. D:\CMD++>. But I can't see a way how to make use of that.
EDIT:
I just noticed there is something wrong with the $+ of prompt that confused me.
The setlocal in the example above actually makes the script return to the initial directory, but the prompt shows a '+' indicating a missing popd.
D:\CMD>test.cmd
D:\CMD>prompt $P$+$g
D:\CMD>setlocal
D:\CMD>pushd e:\DATA
e:\DATA+>goto :EOF
D:\CMD+>popd
D:\CMD>
EDIT:
Due to the hint to the difference between local environment and directory stack I expanded the example form above, pushing to C:\DATA before calling the script. the script returns to where it has been called from (C:\DATA). So far so good. The first popd shows no effect as it removes the last pushd from stack but not changing the directory, since the directory change has been undone by the (implicit) endlocal. The second popd returns to initial directory as expected.
D:\CMD>pushd C:\DATA
C:\DATA+>d:\cmd\test.cmd
C:\DATA+>prompt $P$+$g
C:\DATA+>setlocal
C:\DATA+>pushd e:\DATA
e:\DATA++>goto :EOF
C:\DATA++>popd
C:\DATA+>popd
D:\CMD>
The pushd command without any arguments lists the contents of the directory stack, which can be made use of, by writing the list with output redirection > to a (temporary) file, which is then read by a for /F loop, and to determine how many popd commands are necessary.
In a batch-file:
pushd > "tempfile.tmp" && (
for /F "usebackq delims=" %%P in ("tempfile.tmp") do popd
del "tempfile.tmp"
)
Directly in cmd:
pushd > "tempfile.tmp" && (for /F "usebackq delims=" %P in ("tempfile.tmp") do #popd) & del "tempfile.tmp"
Note, that you cannot use for /F "delims=" %P in ('pushd') do ( … ), because for /F would execute pushd in a new instance of cmd.exe, which has got its own new pushd/popd stack.
One way to prevent residues from the pushd/popd stack is to simply avoid pushd and to use cd /D instead within a localised environment (by setlocal/endlocal in a batch file), because, unlike popd, endlocal becomes implicitly executed whenever necessary (even if the batch file contains wrong syntax that leads to a fatal error that aborts execution, like rem %~). This of course only works when there are no UNC paths to be handled (that is, to be mapped to local paths).
It is advisable to use pushd/popd only in a limited code section with out any branches (by if, goto, etc.). Additionally, ensure to account for potential errors (using conditional execution here):
rem // First stack level:
pushd "D:\main" && (
rem // Second stack level:
pushd "D:\main\sub" && (
rem /* If `D:\main\sub` does not exist, `pushd` would obviously fail;
rem to avoid unintended `popd`, use conditional execution `&&`: */
popd
)
rem // We are still in directory `D:\main` here as expected.
popd
)
Two points here:
1. Setlocal/Endlocal commands save/restore current directory!
This means that when Endlocal command is executed, the current directory is returned to the point it has when the previous Setlocal command was executed. This happens no matter if the Endlocal is explicitly executed, or it is implicitly executed by the termination of the called subroutine (via exit /B or goto :EOF commands).
This behaviour could be used to solve your problem, because all changes of the current directory are reverted when endlocal command is executed. That is:
setlocal
cd somwhere
rem doing stuff
goto :EOF
However note that this behavior is not related to pushd/popd commands! You can not mix the use of this feature with pushd/popd commands. You would need to complete several tests until the mechanism of the interaction between these two features be comprehended.
This is an undocumented behavior discussed here.
2. POPD command sets the Exit Code when there is no previous PUSHD
As described below Exit Code management section at this answer, there are some commands that does not set the ErrorLevel when an error happens, but sets an internal value we could call "Exit Code". This value can be tested via the && || construct this way:
command && Then command when Exit Code == 0 || Else command when Exit code != 0
POPD is one of this type of commands (described under Table 5), so we could use this feature to execute POPD several times until there is not a matching PUSHD previously executed:
#echo off
setlocal
echo Enter in:
for /L %%i in (1,1,%1) do echo %%i & pushd ..
echo Leave out:
set "n=0"
:nextLevel
popd || goto exitLevels
set /A n+=1
echo %n%
goto nextLevel
:exitLevels
echo Done
Bottom line: a simple popd && goto POPBACK line solves your problem...
After my own reminder of exit codes in a now deleted comment and the answer posted by #Aacini, one more version
#echo off
pushd "somewhere"
pushd "somewhere else"
:pop
popd && (echo %cd% & goto :pop) || goto :EOF
Where echo %cd% will only show the each item in the stack, so can be removed and simply become:
:pop
popd && goto :pop || goto :EOF

How to have a called batch file halt the parent [duplicate]

I have a simple function written to check for directories:
:direxist
if not exist %~1 (
echo %~1 could not be found, check to make sure your location is correct.
goto:end
) else (
echo %~1 is a real directory
goto:eof
)
:end is written as
:end
endlocal
I don't understand why the program would not stop after goto:end has been called. I have another function that uses the same method to stop the program and it work fine.
:PRINT_USAGE
echo Usage:
echo ------
echo <file usage information>
goto:end
In this instance, the program is stopped after calling :end; why would this not work in :direxist? Thank you for your help!
I suppose you are mixing call and goto statements here.
A label in a batch file can be used with a call or a goto, but the behaviour is different.
If you call such a function it will return when the function reached the end of the file or an explicit exit /b or goto :eof (like your goto :end).
Therefore you can't cancel your batch if you use a label as a function.
However, goto to a label, will not return to the caller.
Using a synatx error:
But there is also a way to exit the batch from a function.
You can create a syntax error, this forces the batch to stop.
But it has the side effect, that the local (setlocal) variables will not be removed.
#echo off
call :label hello
call :label stop
echo Never returns
exit /b
:label
echo %1
if "%1"=="stop" goto :halt
exit /b
:halt
call :haltHelper 2> nul
:haltHelper
()
exit /b
Using CTRL-C:
Creating an errorcode similar to the CTRL-C errorcode stops also the batch processing.
After the exit, the setlocal state is clean!
See #dbenham's answer Exit batch script from inside a function
Using advanced exception handling:
This is the most powerful solutions, as it's able to remove an arbitrary amount of stack levels, it can be used to exit only the current batch file and also to show the stack trace.
It uses the fact, that (goto), without arguments, removes one element from the stack.
See Does Windows batch support exception handling?
jeb's solution works great. But it may not be appropriate in all circumstances. It has 2 potential drawbacks:
1) The syntax error will halt all batch processing. So if a batch script called your script, and your script is halted with the syntax error, then control is not returned to the caller. That might be bad.
2) Normally there is an implicit ENDLOCAL for every SETLOCAL when batch processing terminates. But the fatal syntax error terminates batch processing without the implicit ENDLOCAL! This can have nasty consequences :-( See my DosTips post SETLOCAL continues after batch termination! for more information.
Update 2015-03-20 See https://stackoverflow.com/a/25474648/1012053 for a clean way to immediately terminate all batch processing.
The other way to halt a batch file within a function is to use the EXIT command, which will exit the command shell entirely. But a little creative use of CMD can make it useful for solving the problem.
#echo off
if "%~1" equ "_GO_" goto :main
cmd /c ^""%~f0" _GO_ %*^"
exit /b
:main
call :label hello
call :label stop
echo Never returns
exit /b
:label
echo %1
if "%1"=="stop" exit
exit /b
I've got both my version named "daveExit.bat" and jeb's version named "jebExit.bat" on my PC.
I then test them using this batch script
#echo off
echo before calling %1
call %1
echo returned from %1
And here are the results
>test jebExit
before calling jebExit
hello
stop
>test daveExit
before calling daveExit
hello
stop
returned from daveExit
>
One potential disadvantage of the EXIT solution is that changes to the environment are not preserved. That can be partially solved by writing the environent to a temporary file before exiting, and then reading it back in.
#echo off
if "%~1" equ "_GO_" goto :main
cmd /c ^""%~f0" _GO_ %*^"
for /f "eol== delims=" %%A in (env.tmp) do set %%A
del env.tmp
exit /b
:main
call :label hello
set junk=saved
call :label stop
echo Never returns
exit /b
:label
echo %1
if "%1"=="stop" goto :saveEnvAndExit
exit /b
:saveEnvAndExit
set >env.tmp
exit
But variables with newline character (0x0A) in the value will not be preserved properly.
If you use exit /b X to exit from the function then it will set ERRORLEVEL to the value of X. You can then use the || conditional processing symbol to execute a command if ERRORLEVEL is non zero.
#echo off
setlocal
call :myfunction PASS || goto :eof
call :myfunction FAIL || goto :eof
echo Execution never gets here
goto :eof
:myfunction
if "%1"=="FAIL" (
echo myfunction: got a FAIL. Will exit.
exit /b 1
)
echo myfunction: Everything is good.
exit /b 0
Output from this script is:
myfunction: Everything is good.
myfunction: got a FAIL. Will exit.
Here's my solution that will support nested routines if all are checked for errorlevel
I add the test for errolevel at all my calls (internal or external)
#echo off
call :error message&if errorlevel 1 exit /b %errorlevel%<
#echo continuing
exit /b 0
:error
#echo in %0
#echo message: %1
set yes=
set /p yes=[no]^|yes to continue
if /i "%yes%" == "yes" exit /b 0
exit /b 1

execute batch files in parallel and get exit code from each

How can I execute set of batch files from single batch file in parallel and get the exit code from each. When I use start it executes the batch file in parallel (new cmd window) but don't return the exit code from each. And while using call, I can get the exit code but the batch file execution happens sequentially. I have following code:
ECHO ON
setlocal EnableDelayedExpansion
sqlcmd -S server_name -E -i select_code.sql >\path\output.txt
for /f "skip=2 tokens=1-3 delims= " %%a in ('findstr /v /c:"-" \path\output.txt') do (
echo $$src=%%a>\path1\%%c.prm
echo $$trg=%%b>>\path1\%%c.prm
set param_name=%%c
start cmd \k \path\exec_pmcmd_ctrm.bat workflow_name %%param_name%%
ping 1.1.1.1 -n 1 -w 5000 > nul
set exitcode="%V_EXITCODE%"
echo %exitcode%>>\path\exitcode.txt
)
This executes the exec_pmcmd_ctrm.bat 3 times with different variable in parallel , but I am unable to get the exit code from each execution. I tried using call but then I miss the parallel execution of bat file. Any help in this regard?
First of all, the "exit code" from another Batch file (that ends with exit /B value command) is taken via %ERRORLEVEL% variable (not %V_EXITCODE%). Also, if such a value changes inside a FOR loop, it must be taken via Delayed Expansion instead: !ERRORLEVEL! (and EnableDelayedExpansion at beginning of your program). However, these points don't solve your problem because there is a misconception here...
When START command is used (without the /WAIT switch), a parallel cmd.exe process start execution. This means that there is not a direct way that the first Batch file could know in which moment the parallel Batch ends in order to get its ERRORLEVEL at that point! There is not a "wait for a started Batch file" command, but even if it would exist, it don't solve the problem of have several concurrent Batch files. In other words, your problem can not be solved via direct commands, so a work around is necessary.
The simplest solution is that the parallel Batch files store their ERRORLEVEL values in a file that could be later read by the original Batch file. Doing that imply a synchronization problem in order to avoid simultaneous write access to the same file, but that is another story...
Here is a pure batch-file solution. Basically, this script executes all batch files located in the same directory simultaneously, where the exit code (ErrorLevel) of each one is written to an individual log file (with the same name as the batch file and extension .log); these files are checked for existence; as soon as such a log file is found, the stored exit code is read and copied into a summary log file, together with the respective batch file name; as soon as all log files have been processed, this script is terminated; the exit code of this script is zero only if all the exit codes of the executed batch files are zero too. So here is the code -- see all the explanatory rem remarks:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Collect available scripts in an array:
set /A "INDEX=0"
for %%J in ("%~dp0*.bat" "%~dp0*.cmd") do (
if /I not "%%~nxJ"=="%~nx0" (
set "ITEM=%%~fJ"
call set "$BATCH[%%INDEX%%]=%%ITEM%%"
set /A "INDEX+=1"
)
)
rem // Execute scripts simultaneously, write exit codes to individual log files:
for /F "tokens=1,* delims==" %%I in ('set $BATCH[') do (
start "" /MIN cmd /C rem/ ^& "%%~fJ" ^& ^> "%%~dpnJ.log" call echo %%^^ErrorLevel%%
)
rem // Deplete summary log file:
> "%~dpn0.log" rem/
rem // Polling loop to check whether individual log files are available:
:POLLING
rem // Give processor some idle time:
> nul timeout /T 1 /NOBREAK
rem /* Loop through all available array elements; for every accomplished script,
rem so its log file is availabe, the related array element becomes deleted,
rem so finally, there should not be any more array elements defined: */
for /F "tokens=1,* delims==" %%I in ('set $BATCH[') do (
rem // Suppress error message in case log file is not yet available:
2> nul (
rem // Read exid code from log file:
set "ERRLEV=" & set "FILE="
< "%%~dpnJ.log" set /P ERRLEV=""
if defined ERRLEV (
rem // Copy the read exit code to the summary log file:
set "NAME=%%~nxJ"
>> "%~dpn0.log" call echo(%%ERRLEV%% "%%NAME%%"
rem // Undefine related array element:
set "%%I="
rem // Store log file path for later deletion:
set "FILE=%%~dpnJ.log"
)
rem // Delete individual log file finally:
if defined FILE call del "%%FILE%%"
)
)
rem // Jump to polling loop in case there are still array elements:
> nul 2>&1 set $BATCH[ && goto :POLLING
rem // Check individual exit codes and return first non-zero value, if any:
set "ERRALL="
for /F "usebackq" %%I in ("%~dpn0.log") do (
if not defined ERRALL if %%I neq 0 set "ERRALL=%%I"
)
if not defined ERRALL set "ERRALL=0"
endlocal & exit /B %ERRALL%
This approach is quite similar to the one I used in the following Improving Batch File for loop with start subcommand, but there the outputs of simultaneously executed commands are collected.

Variable not getting set correctly in a dos batch file on XP

I am trying to write a batch file to be run in a command prompt on XP. I am trying to get a listing of files in a specific path that follow a certain naming convention. I need to copy and rename each file instance to a static name and drop it to a transmission folder.
Since it may take a little while for the file to go in the transmission folder, I need to check before I copy the next file over so that I don't overlay the previous file. I am not able to use SLEEP or TIMEOUT since I don't have the extra toolkit installed. I try to just continually loop back to a START section until the file is sent.
I noticed that if I passed the %%x value set in the for loop that if I loop back to the START section a couple of times, it seems to lose its value and it is set to nothing. So I tried to set a variable to hold the value.
I seem to be having issues with the variable not being set correctly or not cleared. Originally it kept on referencing the first file but now it doesn't seem to be set at all. The ECHO displays the correct the value but the filename variable is empty still.
Does anyone know a better way of doing this? Thanks in advance for your help as I have already wasted a whole day on this!
This is the batch file:
#ECHO "At the start of the loop"
#for %%x in (C:\OUTBOUND\customer_file*) do (
#ECHO "In the loop"
#ECHO "loop value ="
#ECHO %%x
SET filename=%%x
#ECHO "filename ="
#ECHO %filename%
#ECHO ...ARCHIVE OUTBOUND CUSTOMER FILE
archivedatafile --sourcefile="%filename%" --archivefolder="..\archivedata\customer" --retentiondays=0
IF NOT %ERRORLEVEL%==0 GOTO ERROR
PAUSE
:START
IF EXIST l:\OutputFile (
#ping 1.1.1.1 -n 1 -w 30000
GOTO START
) ELSE (
COPY %filename% l:\OutputFile /Y
IF NOT %ERRORLEVEL%==0 GOTO ERROR
PAUSE
)
)
GOTO END
:ERROR
#echo off
#ECHO *************************************************************
#ECHO * !!ERROR!! *
#ECHO *************************************************************
:END
SET filename=
foxidrive has provided a script that should work, but did not provide an explanation as to why your code fails and how he fixed the problems.
You have 2 problems:
1) Your FOR loop is aborted immediately whenever GOTO is executed within you loop. It does not matter where the GOTO target label is placed - GOTO always terminates a loop. Foxidrive's use of CALL works perfectly - the loop will continue once the CALLed routine returns.
2) You attempt to set a variable within a block of code and then reference the new value within the same block. %VAR% is expanded when the statement is parsed, and complicated commands like IF and FOR are parsed once in their entirety in one pass. Actually, any block of code within parentheses is parsed in one pass. So the values of %ERRORLEVEL% and %FILENAME% will be constant - the values that existed before the block was entered.
As Endoro has indicated, one way to solve that problem is to use delayed expansion. Delayed expansion must be enabled by using setlocal enableDelayedExpansion, and then expand the variable using !VAR!. The value is expanded at execution time instead of parse time. Type HELP SET from the command prompt for more information about delayed expansion.
But beware that delayed expansion can cause its own problems when used with a FOR loop because the delayed expansion occurs after the FOR variable expansion: %%x will be corrupted if the value contains a !. This problem can be solved by carefully toggling delayed expansion ON and OFF as needed via SETLOCAL and ENDLOCAL.
Foxidrive's code avoids the entire delayed expansion issue by using CALL. His :NEXT routine is not inside a FOR loop, so all the commands are reparsed each time it is called, so delayed expansion is not required.
This may work - it is untested:
#echo off
ECHO Starting...
for %%x in (C:\OUTBOUND\customer_file*) do call :next "%%x"
echo done
pause
goto :eof
:next
ECHO ...ARCHIVING OUTBOUND CUSTOMER FILE "%~1"
archivedatafile --sourcefile="%~1" --archivefolder="..\archivedata\customer" --retentiondays=0
IF ERRORLEVEL 1 GOTO :ERROR
:loop
echo waiting for file...
ping -n 6 localhost >nul
IF EXIST l:\OutputFile GOTO :loop
COPY "%~1" l:\OutputFile /Y
IF ERRORLEVEL 1 GOTO :ERROR
GOTO :EOF
:ERROR
ECHO *************************************************************
ECHO * !!ERROR!! in "%%x"
ECHO *************************************************************
pause
goto :EOF
try this:
#echo off&setlocal
for %%x in (C:\OUTBOUND\customer_file*) do SET "filename=%%x"
ECHO %filename%
ECHO ...ARCHIVE OUTBOUND CUSTOMER FILE
archivedatafile --sourcefile="%filename%" --archivefolder="..\archivedata\customer" --retentiondays=0
IF NOT %ERRORLEVEL%==0 GOTO:ERROR
PAUSE
:START
IF EXIST l:\OutputFile ping 1.1.1.1 -n 1 -w 30000&GOTO:START
COPY "%filename%" l:\OutputFile /Y
IF NOT %ERRORLEVEL%==0 GOTO:ERROR
PAUSE
GOTO:END
:ERROR
echo off
ECHO *************************************************************
ECHO * !!ERROR!! *
ECHO *************************************************************
:END
SET "filename="
If you use codeblocks (if and for with ( )) and variables with changing values you have to enable delayed expansion. You don't need code blocks in this code, as you can see.

Resources