Batch Limitation - Maximum Recursion while browsin

2020-07-23 08:24发布

问题:

I have come across a big problem while testing my code. And this problem is the annoying message ... "Maximum recursion depth exceeded". Look how it works:

@echo off

REM ---------- MAIN MENU ----------
:Main

echo 1.Supermarket
echo 2.Exit Batch

setlocal
for /f %%A in ('"prompt $H & echo on & for %%B in (1) do rem"') do set "BS=%%A"
set /p Menu=%BS% Type in your option {1,2} followed by ENTER:
if not '%Menu%'=='' set Menu=%Menu:~0,1%
if %Menu% EQU 1 ENDLOCAL & goto Super
if %Menu% EQU 2 goto EOF

REM ---------- SECONDARY MENU ----------
:Super

echo 1.Supermarket
echo   a.Apple
echo   b.Banana

setlocal
for /f %%A in ('"prompt $H & echo on & for %%B in (1) do rem"') do set "BS=%%A"
set /p Menu=%BS% Type in your option {1,a,b} followed by ENTER:
if not '%Menu%'=='' set Menu=%Menu:~0,1%
if %Menu% EQU 1 ENDLOCAL & goto Main
if %Menu% EQU a ENDLOCAL & goto App
if %Menu% EQU b ENDLOCAL & goto Ban

:App

 setlocal enabledelayedexpansion
 set /a cnt=0
 for /f "tokens=* delims= " %%a in (*.apple) do (set /a cnt+=1)
 if %cnt% EQU 0 (
 echo There are %cnt% apples & pause>nul & ENDLOCAL & goto Main
 ) else (
 echo There are %cnt% apples

 [... do many things and after 200 lines]

 echo The total number of apples was %cnt% & pause>nul & ENDLOCAL & goto Main

:Ban
echo This is Banana & pause>nul & goto Main

:EOF

Let's suppose that the user follows this sequence in a row :

1 --> a (First time) --> 1 --> a (second time) --> 1 --> a (third time) etc etc etc 1 --> a (eighth time) --> 1 --> a (ninith time) and, at this moment, you will get the "Maximum recursion depth exceeded". message. If you just keep ignoring this message, the internal calculations of your code will start to behave strangely, even tough it "apparently" seems to have been yielding good results

In fact, the order does not matter. The same thing happens if you keep varying between letter "a" and "b" or more "a" than "b" as:

1 --> b (First time) --> 1 --> a (second time) --> 1 --> a (third time) ... 1 --> a (eighth time) --> 1 --> b (ninth time). Again the "Maximum recursion depth exceeded". message

In addition, if the user types in the option 1 relentlessly (invariably staying at MAIN MENU only) the error message pops out after the twelfth trial.

How to keep the same code structure as above but avoid this error? I am simplifying here but I am talking about a batch of several hundred lines of code and several secondary, tertiary, quaternary etc submenus.

Thanks,
Maleck

回答1:

I don't see any recursion in the code you posted, and not surprisingly, I can't get the code to fail.

You must have recursion in code that you are not showing that is causing the problem.

I am aware of two types of fatal recursion errors with batch files:

1) SETLOCAL is limited to a maximum of 32 levels. Attempting to use SETLOCAL a 33rd time without issuing an ENDLOCAL will result in the following fatal error message:

Maximum setlocal recursion level reached.

I suspect this is the recursion limit you are reaching.

UPDATE: Actually, the limit is 32 SETLOCAL per CALL level. For more information, read the entire thread at http://ss64.org/viewtopic.php?id=1778

2) CALL recursion can fail if you issue too many CALLs before any of them return. Batch returns from a CALL whenever it reaches the end of the script, EXIT /B, or GOTO :EOF. The maximum number of recursive CALLs is machine dependent. (Perhaps code dependent?) The error message will look something like this:

******  B A T C H   R E C U R S I O N  exceeds STACK limits ******
Recursion Count=600, Stack Usage=90 percent
******       B A T C H   PROCESSING IS   A B O R T E D      ******

There may be some system configuration that could increase the number of recursive CALLs you can make, but there will always be some upper limit.


If you are reaching a recursion limit, then you really don't have much choice other than to restructure your code. But unless you show your recursive code, it is difficult (nigh impossible as far as I'm concerned) for us to suggest a way for you to work around the problem.


EDIT in response to updated question and comment

Here is a general rule that you need to follow: If you have a code loop then every SETLOCAL must be paired with an ENDLOCAL. The loop could be created by GOTO, or it could be a FOR loop.

You are making heavy use of GOTO loops. You successfully identified that after issuing SETLOCAL you must issue ENDLOCAL before you execute a GOTO that loops back. It looks to me like you solved that problem.


One last EDIT - Use CALL to simplify the logic

It is a pain to keep track of exactly where and how many times you must issue ENDLOCAL when you loop with a GOTO. The logic is much simpler if you use CALL instead, and the code is more structured as well.

All SETLOCAL statements that were issued during execution of a CALLed subroutine are implicitly ended once the routine ends. There is no need to explicitly use ENDLOCAL when using CALL.

Below I've shown how your code could be restructured to use CALL. Study all of the code - I've made a number of subtle changes that I think simplify and/or improve other aspects of your logic.

@echo off
setlocal
:: Define the BS variable just once at the beginning
for /f %%A in ('"prompt $H & echo on & for %%B in (1) do rem"') do set "BS=%%A"

:Main  ---------- MAIN MENU ----------
:: ECHO( echoes a blank line
echo(
echo Main Menu
echo   1.Supermarket
echo   2.Exit Batch
:: If user hits ENTER without entering anything then current value remains.
:: So clear the value to make sure we don't accidently take action based
:: on prior value.
set "Menu="
set /p Menu=%BS% Type in your option {1,2} followed by ENTER:
if defined Menu set "Menu=%Menu:~0,1%"
if "%Menu%" EQU "1" call :Super
if "%Menu%" EQU "2" exit /b
goto :Main

:Super ---------- SECONDARY MENU ----------
setlocal
echo(
echo Supermarket
echo   a.Apple
echo   b.Banana
echo   1.Retutn to Main
set "Menu="
set /p Menu=%BS% Type in your option {1,a,b} followed by ENTER:
if defined Menu set "Menu=%Menu:~0,1%"
:: Use IF /I option so case does not matter
if /i "%Menu%" EQU "1" exit /b
:: Note that :App and :Ban are still logially part of this subroutine
:: because I used GOTO instead of CALL. When either :App or :Ban ends, then
:: control will return to the Main menu
if /i "%Menu%" EQU "a" goto :App
if /i "%Menu%" EQU "b" goto :Ban
goto :Super

:App
setlocal enableDelayedExpansion
set /a cnt=0
for /f "tokens=* delims= " %%a in (*.apple) do (set /a cnt+=1)
echo There are %cnt% apples
if %cnt% GTR 0 (
 REM ... do many things and after 200 lines
 echo The total number of apples was %cnt%
)
pause>nul
exit /b
:: The EXIT /B above returns to the Main menu (ends the CALL to :Super)
:: Note that the SETLOCAL at the start of :Super and the one at the
:: start of :App are implicitly ended. No need to explictly issue
:: ENDLOCAL.

:Ban
echo This is Banana
pause>nul
exit /b
:: The EXIT /B above returns to the Main menu (ends the CALL to :Super)
:: Note that the SETLOCAL at the start of :Super is implicitly ended.
:: No need to explictly issue ENDLOCAL.