在函数内部退出批处理脚本

20
我有一个批处理文件的问题。 它通过执行以下操作自动构建多个程序:
  • 设置一些编译标志
  • 运行“gmake all”
  • 调用“检查错误级别”函数,如果错误级别为1,则退出
因此,它看起来像这样:
set FLAG=1
...
gmake all
call :interactive_check
set OTHERFLAG=1
...
gmake all
call :interactive_check

这里有6或7个这样的内容(可能会增加)。因此,我编写了一个函数来检查错误级别,而不是在每个步骤中都复制/粘贴它。 问题是:通过函数进行错误检查:

:interactive_check
if errorlevel 1 (
echo.
echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
echo Error in compilation process... exiting
echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
echo.
cd %root_dir%
exit /B 1
) ELSE (
echo.Continuing to next step
)
goto:eof

现在,当运行它时,exit /B 1仅仅退出了函数,但并没有退出批处理文件本身。

您是否知道如何退出整个批处理文件而不必在每个步骤中复制/粘贴我的"if errorlevel 1.."?


2
你可以这样做。只需将 call :interactive_check 替换为 if errorlevel 1 goto error 即可。复制粘贴前者和后者之间没有太大区别。 :) - atzz
明白了,用这个方法解决了问题,现在运行良好,谢谢! - Gui13
6个回答

24

有关更好的解决方案,请参见改进版本的部分

您可以在任何时候停止批处理,即使在嵌套函数调用内部也是如此。

您只需要创建一个语法错误,例如使用空块(),以抑制错误消息,它可以在调用中执行,并将调用的stderr重定向到nul。

@echo off
setlocal enabledelayedexpansion

rem Do something
call :interactive_check

rem Do something
call :interactive_check

goto :eof

::::::::::::::::::::::::::
:interactive_check
if errorlevel 1 (
    echo.
    echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
    echo Error in compilation process... exiting
    echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
    call :halt 1
) ELSE (
    echo.Continuing to next step
)
goto :eof

:: Sets the errorlevel and stops the batch immediately
:halt
call :__SetErrorLevel %1
call :__ErrorExit 2> nul
goto :eof

:__ErrorExit
rem Creates a syntax error, stops immediately
() 
goto :eof

:__SetErrorLevel
exit /b %time:~-2%
goto :eof

2017-04-09 改进版:仅退出当前批处理,而不退出调用方批处理

正如@dbenham所提到的,有一种新的异常处理技术,也可以用于仅退出当前批处理。

@echo off
echo Do something, detecting some fatal error
call :ExitBatch 3
exit /b

:ExitBatch [errorcode] - Exits only the current batch file, regardless how many CALLs
set _errLevel=%1
REM *** Remove all calls from the current batch from the call stack
:popStack
(
    (goto) 2>nul
    setlocal DisableDelayedExpansion    
    call set "caller=%%~0"
    call set _caller=%%caller:~0,1%%
    call set _caller=%%_caller::=%%
    if not defined _caller (
        REM callType = func
        rem set _errLevel=%_errLevel%
        goto :popStack
    )   
    (goto) 2>nul
    endlocal
    cmd /c "exit /b %_errLevel%"
)
echo NEVER REACHED
exit /b

很棒的解决方案 :) 我使用了atzz的建议来解决问题,但如果有人在这里遇到同样的问题,你就是答案。 - Gui13
1
这个解决方案会在SETLOCAL之后不正确地保留环境更改。我已经发现了一个干净的解决方案,可以在SETLOCAL之后正确地丢弃所有更改。 - dbenham

24
使用致命的语法错误,就像jeb所演示的那样,确实可以终止所有批处理,但它也有一个令人讨厌的副作用——即使在批处理结束时通过隐式ENDLOCAL应该被丢弃,但SETLOCAL后的环境更改仍然会被保留。有关详细信息,请参阅我的DosTips帖子“SETLOCAL continues after batch termination!”
根据“为什么非交互式批处理脚本认为我按了Control-C?”上的信息,我发现了一种干净的方法来从调用的例程或脚本中退出所有批处理,并且在SETLOCAL之后所有更改都被正确地丢弃。
@echo off
setlocal
set test=AFTER main SETLOCAL
call :sub
echo returning from main  NEVER REACHED
exit /b

:sub
setlocal
set test=AFTER sub SETLOCAL
set test
call :ExitBatch
echo returning from sub2 NEVER REACHED
exit /b


:ExitBatch - Cleanly exit batch processing, regardless how many CALLs
if not exist "%temp%\ExitBatchYes.txt" call :buildYes
call :CtrlC <"%temp%\ExitBatchYes.txt" 1>nul 2>&1
:CtrlC
cmd /c exit -1073741510

:buildYes - Establish a Yes file for the language used by the OS
pushd "%temp%"
set "yes="
copy nul ExitBatchYes.txt >nul
for /f "delims=(/ tokens=2" %%Y in (
  '"copy /-y nul ExitBatchYes.txt <nul"'
) do if not defined yes set "yes=%%Y"
echo %yes%>ExitBatchYes.txt
popd
exit /b

以下是运行上述test.bat的示例输出。您可以看到,脚本从:ExitBatch调用中未返回,并且一旦批处理终止,test变量定义已被正确丢弃。

C:\test>test.bat
test=AFTER sub SETLOCAL

C:\test>set test
Environment variable test not defined

C:\test>

可以将 :ExitBatch 程序放入自己的 ExitBatch.bat 脚本文件中,并将其放置在 PATH 中的某个位置,以便任何批处理脚本都可以方便地使用。
@echo off
:ExitBatch - Cleanly exit batch processing, regardless how many CALLs
if not exist "%temp%\ExitBatchYes.txt" call :buildYes
call :CtrlC <"%temp%\ExitBatchYes.txt" 1>nul 2>&1
:CtrlC
cmd /c exit -1073741510

:buildYes - Establish a Yes file for the language used by the OS
pushd "%temp%"
set "yes="
copy nul ExitBatchYes.txt >nul
for /f "delims=(/ tokens=2" %%Y in (
  '"copy /-y nul ExitBatchYes.txt <nul"'
) do if not defined yes set "yes=%%Y"
echo %yes%>ExitBatchYes.txt
popd
exit /b

重要更新:

现在可以使用纯批处理实现强大的异常处理。不仅可以抛出异常并终止任何调用级别的批处理,还可以在更高级别捕获异常,执行特殊的异常处理清理代码,并恢复处理,或通过另一个throw继续退出!有关更多信息,请参见Windows批处理是否支持异常处理?


聪明。很高兴你能如此迅速地利用我的发现;这几乎弥补了最初让我感到困惑的事情。 :-) - Harry Johnston
不错的解决方案,对于正常情况比我的更简洁。 - jeb

6

当您使用exit /b X从函数中退出时,它会将ERRORLEVEL设置为X的值。然后,您可以使用|| 条件处理符号call之后执行另一个命令,如果ERRORLEVEL非零。

@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

这个脚本的输出结果是:
myfunction: Everything is good.
myfunction: got a FAIL. Will exit.

2
我建议采用以下解决方案:
@echo off

:: comment next line if you want to export any local variables in caller environment
setlocal

set FLAG=1
rem Do something
call :interactive_check

set FLAG2=2
rem Do something
call :interactive_check

goto :eof

:interactive_check
if errorlevel 1 (
    echo.
    echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
    echo Error in compilation process... exiting
    echo /!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\/!\
    (goto) 2>nul & endlocal & exit /b %ERRORLEVEL%
) else (
    echo Continuing to next step
)
goto :eof

它会保存产生错误的命令的错误代码。如果您想将错误代码保存到特定值,则修改以下行:

(goto) 2>nul & endlocal & exit /b YOUR_EXITCODE_HERE

我不喜欢有语法错误的解决方案(请参见jeb的帖子),因为它会终止当前批处理文件的调用者。


1
超出堆栈限制将立即终止所有批处理:
@echo off

echo I am %0, calling :subroutine
call:subroutine

NEVER PARSED
exit /b


:subroutine
echo I am %0, calling :recurse
echo Exiting batch script IMMEDIATELY

cmd /c exit /b %=EXIT CODE HERE=%
2>nul call:recurse

NEVER PARSED
exit /b


:recurse - Aborts all batch processing, regardless of CALL depth
call:recurse

NEVER PARSED
exit /b

最好和最快的解决批处理进程的方法,太好了! - CooderSK

0

大多数解决方案仅适用于2个堆栈级别或带有屏幕错误消息。

因此,我想出了两个解决方案,我认为它们都比我在这里看到的更好。

没有SETLOCAL和ERRORLEVEL的复杂脚本解决方案

当以下条件满足时,可以使用此解决方案:

  • 您有太多递归或条件函数,不知道程序何时终止
  • 您不关心留在环境中的变量
  • 您不关心errorLevel
CALL :leave 2>nul
:leave
()
GOTO :eof

解释:它将call :leave放在堆栈级别上,并且伪参数2>nul将错误消息发送到void,从而抑制该代码块中的错误。然后()将生成致命错误,将杀死整个程序。

您甚至可以通过在主函数中设置SET空白或放置ENDLOCAL来删除变量CALL :leave之前。 但是,在复杂的程序中,您不知道何时执行此操作。但在这种情况下,我想出了更好的解决方案...

我知道许多人在这里看到过这段代码:(GOTO) 2>nul & ENDLOCAL & EXIT /b %yourCode%,但正如我所说,它只会终止当前函数并终止调用它的上一个函数。如果您有多个函数在堆栈上相互调用,则无法正常工作。因此,您可以强制发生多个错误:

(GOTO) 2>nul & (GOTO) 2>nul & (GOTO) 2>nul & ENDLOCAL & EXIT /b

然而,您不知道要执行多少次,如果过度执行,甚至可能会杀死:main并执行一个exit / b,这将关闭控制台。然后我有

使用SETLOCAL和ERRORLEVEL解决复杂脚本的方案

此解决方案适用于以下情况:

  • 您有太多递归或条件函数,不确定程序何时终止
  • 您不想留下残余变量
  • 您检查errorLevel(可选)
SET dummy=iAmDefined
FOR /l %%i in (0, 1, 1000) do (
  IF defined dummy (
    (GOTO) 2>nul & IF NOT defined dummy (CMD /c EXIT /b %yourExitCode%)
  )
)

它不会运行一千次,而只会运行尽可能少的次数,直到!dummy!错过分配。

除非您想要调试!dummy!回显,否则无需启用EnableDelayedExpansion。

示例:

@ECHO off
SETLOCAL EnableDelayedExpansion
SET dummy=iAmDefined

CALL :func3

:func1
ECHO i'm in func1
CALL :leave 2>nul
::GOTO :eof

:func2
ECHO i'm in func2
CALL: func1
::GOTO :eof

:func3
ECHO i'm in func3
CALL: func2
::GOTO :eof

:leave
ECHO ===leaving===
FOR /l %%i in (1, 1, 1000) of (
  IF defined dummy (
    (GOTO) & ECHO %%i !dummy! & IF NOT defined dummy (CMD /c EXIT /b 555)
  )
)
::GOTO :eof
::ENDLOCAL

退出 e 解释:

             -- 1 call  (called func3)
i'm in func3 -- 2 calls (called func2)
i'm in func2 -- 3 calls (called func1)
i'm in func1 -- 4 calls (called leave)
===leaving===
1 iAmDefined -- rollback :leave
2 iAmDefined -- rollback :func3
3 iAmDefined -- rollback :func2
4 iAmDefined -- rollback :func1
5 !dummy!    -- rollback :main before unset dummy
                new CMD and exit after unset dummy

在第5个虚拟点之前仍然设置了(GOTO),但ECHO是在之后。 :EOF被注释以说明这些gotoes未被使用


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接