批处理限制——在浏览菜单时最大的递归。

时间:2022-12-23 22:18:40

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

1——> a(第一次)> 1 -> a(第二次)-> 1 -> a(第三次)等1 -> a(第8次)-> 1 -> a (ninith time),此时,你将得到“最大递归深度超过”。消息。如果你一直忽视这条信息,你的代码的内部计算将开始表现得奇怪,甚至是“很明显”似乎已经产生了良好的结果。

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:

事实上,顺序无关紧要。如果你在字母“a”和“b”或“a”比“b”之间保持不同,则会发生同样的情况:

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

1 - - > b(第一次)- - > 1 - >(第二次)- - > 1 - >(第三次)…1——> a(第8次)——> 1——> b(第九次)。再一次,“最大递归深度超过”。消息

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.

此外,如果用户在选项1中坚持不懈地(只在主菜单中停留),那么在第12次试验之后会弹出错误消息。

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

谢谢,Maleck

1 个解决方案

#1


9  

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:

1)SETLOCAL的最大值为32。尝试使用SETLOCAL第33次而不发出ENDLOCAL将导致以下致命错误消息:

Maximum setlocal recursion level reached.

达到了最大的setlocal递归级别。

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

更新:实际上,每个调用级别的限制是32个SETLOCAL。要了解更多信息,请阅读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:

如果在返回之前发出太多调用,则调用递归会失败。当它到达脚本的末尾,EXIT /B或GOTO:EOF时,从一个调用返回。递归调用的最大数量是依赖于机器的。(也许代码依赖?)错误消息将如下所示:

******  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.

这里有一个您需要遵循的一般规则:如果您有一个代码循环,那么每个SETLOCAL必须与ENDLOCAL配对。该循环可以由GOTO创建,也可以是FOR循环。

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.

你正在大量使用GOTO循环。您成功地识别出,在发出SETLOCAL后,必须在执行回滚之前,先发出ENDLOCAL。在我看来,你解决了那个问题。


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.

当您使用GOTO循环时,要准确地记录您必须在什么地方、多少次发送ENDLOCAL,这是一种痛苦。如果使用调用,那么逻辑就简单多了,而且代码也更结构化。

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.

在执行调用子例程时发出的所有SETLOCAL语句在例程结束时隐式地结束。在使用调用时,不需要显式地使用ENDLOCAL。

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.

#1


9  

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:

1)SETLOCAL的最大值为32。尝试使用SETLOCAL第33次而不发出ENDLOCAL将导致以下致命错误消息:

Maximum setlocal recursion level reached.

达到了最大的setlocal递归级别。

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

更新:实际上,每个调用级别的限制是32个SETLOCAL。要了解更多信息,请阅读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:

如果在返回之前发出太多调用,则调用递归会失败。当它到达脚本的末尾,EXIT /B或GOTO:EOF时,从一个调用返回。递归调用的最大数量是依赖于机器的。(也许代码依赖?)错误消息将如下所示:

******  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.

这里有一个您需要遵循的一般规则:如果您有一个代码循环,那么每个SETLOCAL必须与ENDLOCAL配对。该循环可以由GOTO创建,也可以是FOR循环。

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.

你正在大量使用GOTO循环。您成功地识别出,在发出SETLOCAL后,必须在执行回滚之前,先发出ENDLOCAL。在我看来,你解决了那个问题。


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.

当您使用GOTO循环时,要准确地记录您必须在什么地方、多少次发送ENDLOCAL,这是一种痛苦。如果使用调用,那么逻辑就简单多了,而且代码也更结构化。

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.

在执行调用子例程时发出的所有SETLOCAL语句在例程结束时隐式地结束。在使用调用时,不需要显式地使用ENDLOCAL。

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.