1

I would like to write a simple Windows batch script that, given a directory containing a set of subdirectories, loops through them all and calls a batch script of the same name in each. The code I have tried is like this:


rem Run tests, starting with test # given as arg
IF [%1] == [] (SET testnum="0") ELSE (SET testnum=%1)

echo arg1 is %1
echo Start test = %testnum%

rem We can't increment J in the for loop because 
rem braindead CMD doesn't work that way...
SET J=1
FOR /D %%I IN (*) DO (call :subroutine "%%I")
GOTO :eof

rem Instead we do it in a subroutine
rem but for some reason this starts with the last test
rem and ends with test-1... why?
:subroutine
    IF %J% GEQ %testnum% (
    SET test=test%J%
    echo %test%
rem     PUSHD %%I 
rem     CALL run.bat 
rem     POPD
    )
    SET /A J+=1

This almost works, except (for example) if I have 196 subdirectories like test1test196, if called with an argument of 100 it prints out:

test196
test100
...
test195

I can't see a reason why the final value is called first.

What am I missing?

0

3 Answers 3

1

I suggest the following with command REM commented batch file for this task.

@ECHO OFF
SETLOCAL EnableExtensions DisableDelayedExpansion
REM Make the batch file directory the current working directory.
PUSHD "%~dp0" || EXIT /B
REM Run tests, starting with test# given as argument.
IF "%~1" == "" (SET "testnum=0") ELSE SET "testnum=%~1"

ECHO Argument 1 is: %1
ECHO Start test = %testnum%

REM Process all non-hidden subdirectories in the batch file directory of
REM which name begins with the string test. Use FOR /F command to get just
REM the number of the subdirectory assigned to the loop variable J which
REM must not have leading zeros. Check next if that number is greater or
REM equal the number assigned to the variable testnum. In this case check
REM if the subdirectory contains the batch file with name run.bat. If this
REM condition is true, make the subdirectory the current working directory,
REM call the batch file run.bat in the subdirectory, and then restore the
REM batch file directory as the current working directory.
FOR /D %%I IN (test*) DO FOR /F "delims=ESTest" %%J IN ("%%I") DO IF %%J GEQ %testnum% IF EXIST "%%I\run.bat" (
    PUSHD "%%I"
    CALL run.bat
    POPD
)
REM Restore the initial current working directory on start of the batch file.
POPD
REM Restore the initial local environment on start of the batch file.
ENDLOCAL

In which order the subdirectories matched by the wildcard pattern test* are processed depends on the file system. NTFS and ReFS store file system entries sorted alphabetically (locale specific as on creation of the partition) while FAT32 or exFAT store file system entries according to last creation/modification with the newest created/modified files/directories at bottom of the list.

Example: The subdirectories are created in this order:

  1. test1
  2. test4
  3. test3
  4. test2
  5. test121
  6. test14

NTFS and ReFS store internally the directory names in the following order:

  1. test1
  2. test121
  3. test14
  4. test4
  5. test3
  6. test2

FAT32 and exFAT store internally the directory names in the following order:

  1. test1
  2. test4
  3. test3
  4. test2
  5. test121
  6. test14

The directories names matched by the wildcard pattern test* are returned by the file system to cmd.exe processing the batch file in the order as stored in file system. The batch file above does not contain any commands to first get the list of directory names loaded into the memory of cmd.exe in an alphanumeric order as the Windows File Explorer displays them. Windows File Explorer displays the directories in the following order:

  1. test1
  2. test2
  3. test3
  4. test4
  5. test14
  6. test121

In does not really matter in which order run.bat is executed as long as all run.bat in a test* subdirectory with a number greater or equal the number specified on execution of the main batch file as first argument are called by the main batch file.

More command lines would be necessary to get the directory names first sorted alphanumeric like Windows File Explorer does before processing the directory names with a FOR loop.

An alphanumeric sort with test1, test2test9, test10, test11test99, test100, test101 … is not trivial as there is no Windows command which supports such a sort.

A processing in correct alphanumeric order would be easier to achieve with all subdirectories having the same number of digits, i.e. test001, test002test009, test010, test011test099, test100, test101

The batch file for such directory names could be:

@ECHO OFF
SETLOCAL EnableExtensions DisableDelayedExpansion
REM Make the batch file directory the current working directory.
PUSHD "%~dp0" || EXIT /B
REM Run tests, starting with test# given as argument.
IF "%~1" == "" (SET "testnum=0") ELSE SET "testnum=%~1"

ECHO Argument 1 is: %1
ECHO Start test = %testnum%

REM Process alphabetic ordered by name all non-hidden subdirectories not
REM being a junction or symbolic link in the batch file directory of which
REM name begins with the string test. The subdirectory names must have all
REM the same length, i.e. the number of digits must be the same in all the
REM directory names. Use FOR /F command to get just the number of the
REM subdirectory without leading zeros assigned to the loop variable J.
REM Check next if that decimal interpreted number is greater or equal
REM the number assigned to the variable testnum. In this case check if
REM the subdirectory contains the batch file with name run.bat. If this
REM condition is true, make the subdirectory the current working directory,
REM call the batch file run.bat in the subdirectory, and then restore the
REM batch file directory as the current working directory.
FOR /F %%I IN ('DIR test* /AD-H-L /B /ON 2^>NUL') DO FOR /F "tokens=* delims=0ESTest" %%J IN ("%%I") DO IF %%J GEQ %testnum% IF EXIST "%%I\run.bat" (
    PUSHD "%%I"
    CALL run.bat
    POPD
)
REM Restore the initial current working directory on start of the batch file.
POPD
REM Restore the initial local environment on start of the batch file.
ENDLOCAL

The following batch file calls run.bat in correct order for the directory naming schemes:

  1. test1, test2test9, test10, test11test99, test100, test101
  2. test001, test002test009, test010, test011test099, test100, test101

But it does not call run.bat in the correct numeric order if the directory names are a mixture of both naming schemes, i.e. some directory names with a number less than 100 are without and some with leading zeros.

@ECHO OFF
SETLOCAL EnableExtensions DisableDelayedExpansion
REM Make the batch file directory the current working directory.
PUSHD "%~dp0" || EXIT /B
REM Run tests, starting with test# given as argument.
IF "%~1" == "" (SET "testnum=0") ELSE SET "testnum=%~1"

ECHO Argument 1 is: %1
ECHO Start test = %testnum%

REM Process alphanumeric ordered by name all non-hidden subdirectories not
REM being a junction or symbolic link in the batch file directory of which
REM name begins with the string test. Use FOR /F command to get just the
REM number of the subdirectory without leading zeros assigned to the loop
REM variable J. Check next if that decimal interpreted number is greater
REM or equal the number assigned to the variable testnum. In this case check
REM if the subdirectory contains the batch file with name run.bat. If this
REM condition is true, make the subdirectory the current working directory,
REM call the batch file run.bat in the subdirectory, and then restore the
REM batch file directory as the current working directory.
FOR /F %%I IN ('DIR test* /AD-H-L /B /ON 2^>NUL ^| %SystemRoot%\System32\findstr.exe /I /R "^test[123456789]$" ^& DIR test* /AD-H-L /B /ON 2^>NUL ^| %SystemRoot%\System32\findstr.exe /I /R "^test[0123456789][0123456789]$" ^& DIR test* /AD-H-L /B /ON 2^>NUL ^| %SystemRoot%\System32\findstr.exe /I /R "^test[0123456789][0123456789][0123456789]$"') DO FOR /F "tokens=* delims=0ESTest" %%J IN ("%%I") DO IF %%J GEQ %testnum% IF EXIST "%%I\run.bat" (
    PUSHD "%%I"
    CALL run.bat
    POPD
)
REM Restore the initial current working directory on start of the batch file.
POPD
REM Restore the initial local environment on start of the batch file.
ENDLOCAL

I recommend to read further:

  • Why is no string output with 'echo %var%' after using 'set var = text' on command line?
    It explains the difference between SET testnum="0" with first " after the equal sign and SET "testnum=0" with first " left to the variable name. SET testnum="0" is wrong for this use case using %testnum% in an integer comparison with GEQ.
  • Symbol equivalent to NEQ, LSS, GTR, etc. in Windows batch files
    It explains the reason why IF %J% GEQ %testnum% with SET testnum="0" resulted in a string instead of an integer comparison on execution of the batch file in the question while IF %%J GEQ %testnum% and SET "testnum=0" in the batch files posted in this answer really results in an integer comparison. There is also explained why it is necessary to remove leading zeros from the number by the second batch file to avoid interpreting 014 as octal number (decimal 12) and 008 or 092 or other numbers with a leading zero with digit 8 or 9 as invalid octal number and therefore as decimal number 0.
Sign up to request clarification or add additional context in comments.

Comments

1

Your problem comes from the way cmd.exe parses variables inside a FOR loop.

In a FOR loop, %variables% are expanded when the loop is parsed, not when it runs. This means %J% inside the loop stays constant during execution, which is why the order appears messed up (last directory first, etc.).

To fix this, you need Delayed Variable Expansion.

Here’s a corrected version of your script:

@echo off
setlocal enabledelayedexpansion

rem Default starting test number
if "%~1"=="" (
    set testnum=0
) else (
    set testnum=%~1
)

echo arg1 is %1
echo Start test = %testnum%

set J=1

for /D %%I in (*) do (
    call :subroutine "%%I"
)

goto :eof

:subroutine
    if !J! GEQ %testnum% (
        set test=test!J!
        echo !test!
        rem pushd %~1
        rem call run.bat
        rem popd
    )
    set /A J+=1
goto :eof

2 Comments

THanks, that avoids starting with the last-1 test. But it still finishes with the last-1 test i.e. running the script with dirs test1...test196, and specifying arg=100, it prints test100....test195. I'd like it to finish on the last test, test196.
I guess setting J=2 as the loop counter start value is the easiest way of getting it to stop on the last test.
1

For me, your original code yielded

Original code, arg=8, 15 'test' directories


arg1 is 8
Start test = 8
ECHO is off.
test8
test9
test10
test11
test12
test13
test14

which differs from your narrative.

Here's some code that works:

@ECHO OFF
SETLOCAL

SET "sourcedir=u:\your files"
PUSHD "%sourcedir%"

rem Run tests, starting with test # given as arg
SET "testnum=%~1"&IF NOT DEFINED testnum SET /a testnum=1

echo arg1 is %1
echo Start test = %testnum%

:: We haven't yet attempted a test

SET currenttest=0

FOR /D %%I IN (*) DO (SET /a currenttest+=1&call :subroutine "%%I")

POPD

GOTO :eof

:subroutine
IF %currenttest% lss %testnum% GOTO :eof
SET test=test%currenttest%
echo %test%
rem     PUSHD %~1 
rem     CALL run.bat 
rem     POPD
GOTO :EOF

Notes:

Variable J changed to a more meaningful name (currenttest)

I've set the test to my test directory, u:\your files containing 15 subdirectories named test1..test15 for testing.

A 'pushd/popd` bracket is included to set the current directory to the test directory for, well, testing.

The subroutine uses %~1 as the directory on which to run run.bat. I'd sugest changing the rem statements to echo for verification.

So - if no parameter, testnum is set to nothing hence is undefined, so adjusted to 1 (the first test to be performed)

Then only run the call run code if the current test is not less than testnum.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.