web-dev-qa-db-ja.com

Windowsバッチは例外処理をサポートしていますか?

Windowsバッチプログラミングは例外処理をサポートしていますか?そうでない場合、バッチファイル内で例外処理を効果的にエミュレートする方法はありますか?

バッチスクリプト内の任意のCALLレベルで "例外をスロー"し、アクティブな "TRYブロック"が見つかるまでCALLスタックを繰り返しポップさせ、 "CATCHブロック"が例外を処理できるようにしたい完全に続行するか、何らかのクリーンアップを行ってCALLスタックのポップを続行します。例外が処理されない場合、バッチ処理は終了し、制御はエラーメッセージとともにコマンドラインコンテキストに戻ります。

すでに CALLの深さでバッチ処理を終了するための投稿された方法を組み合わせて がありますが、これらの手法はいずれも、例外処理によって他の言語で通常提供される構造化されたクリーンアップアクティビティを許可しません。

注:これはすでに良い答えを知っている場合ですそれは最近発見されたばかりで、情報を共有したいです

26
dbenham

Windowsバッチスクリプトには確かに正式な例外処理はありません-言語がいかに原始的であることを考えると、驚くことはほとんどありません。私の夢の中で、効果的な例外処理がハッキングされるとは思いませんでした。

しかし、その後、いくつかの驚くべき発見が ロシアのサイト で、誤ったGOTOステートメントの動作に関して行われました(言われたことはわかりませんが、ロシア語が読めません)。 英語の要約がDosTipsに投稿されました 、および動作がさらに調査されました。

(GOTO) 2>NULはEXIT/Bとほぼ同じように動作することがわかります。ただし、既に解析されたコードブロック内の連結コマンドは、有効なリターンの後も実行されます。CALLerのコンテキスト内!

以下は、ほとんどの顕著な点を示す短い例です。

@echo off
setlocal enableDelayedExpansion
set "var=Parent Value"
(
  call :test
  echo This and the following line are not executed
  exit /b
)
:break
echo How did I get here^^!^^!^^!^^!
exit /b

:test
setlocal disableDelayedExpansion
set "var=Child Value"
(goto) 2>nul & echo var=!var! & goto :break
echo This line is not executed

:break
echo This line is not executed

-出力-

var=Parent Value
How did I get here!!!!

この機能はまったく予想外であり、非常に強力で便利です。次の目的で使用されています。

  • 作成 PrintHere.bat -「nix hereドキュメント機能のエミュレーション」
  • RETURN.BATユーティリティ を作成します。すべてのバッチ「関数」は、実質的に制限なしに、ENDLOCALバリアを越えて任意の値を返すようにCALLできます。コードは jebの元のアイデア の肉付けされたバージョンです。

リストに例外処理を追加することもできます:-)

この手法は、EXCEPTION.BATと呼ばれるバッチユーティリティに依存して、TRY/CATCHブロックの指定および例外のスローに使用される環境変数「マクロ」を定義します。

TRY/CATCHブロックを実装する前に、次を使用してマクロを定義する必要があります。

call exception init

次に、TRY/CATCHブロックが次の構文で定義されます。

:calledRoutine
setlocal
%@Try%
  REM normal code goes here
%@EndTry%
:@Catch
  REM Exception handling code goes here
:@EndCatch

例外は次の方法でいつでもスローできます。

call exception throw  errorNumber  "messageString"  "locationString"

例外がスローされると、アクティブなTRY/CATCHが見つかるまで(GOTO) 2>NULを使用してCALLスタックを繰り返しポップし、CATCHブロックに分岐してそのコードを実行します。 CATCHブロックでは、一連の例外属性変数を使用できます。

  • exception.Code-数値の例外コード
  • exception.Msg-例外メッセージ文字列
  • exception.Loc-例外がスローされた場所を説明する文字列
  • exception.Stack-CATCHブロック(またはキャッチされなかった場合はコマンドライン)から例外Originまでの呼び出しスタックをトレースする文字列。

例外が完全に処理された場合、call exception clearを介して例外をクリアする必要があり、スクリプトは正常に続行されます。例外が完全に処理されていない場合は、新しいexception.Stackを使用して新しい例外をスローするか、古いスタックを保持できます

call exception rethrow  errorNumber  "messageString"  "locationString"

例外が処理されない場合、4つの例外属性を含む「未処理の例外」メッセージが出力され、すべてのバッチ処理が終了し、制御がコマンドラインコンテキストに返されます。

これをすべて可能にするコードを次に示します。完全なドキュメントがスクリプトに埋め込まれ、exception helpまたはexception /?を介してコマンドラインから利用できます。

EXCEPTION.BAT

::EXCEPTION.BAT Version 1.4
::
:: Provides exception handling for Windows batch scripts.
::
:: Designed and written by Dave Benham, with important contributions from
:: DosTips users jeb and siberia-man
::
:: Full documentation is at the bottom of this script
::
:: History:
::   v1.4 2016-08-16  Improved detection of command line delayed expansion
::                    using an original idea by jeb
::   v1.3 2015-12-12  Added paged help option via MORE
::   v1.2 2015-07-16  Use COMSPEC instead of OS to detect delayed expansion
::   v1.1 2015-07-03  Preserve ! in exception attributes when delayed expansion enabled
::   v1.0 2015-06-26  Initial versioned release with embedded documentation
::
@echo off
if "%~1" equ "/??" goto pagedHelp
if "%~1" equ "/?" goto help
if "%~1" equ "" goto help
shift /1 & goto %1


:throw  errCode  errMsg  errLoc
set "exception.Stack="
:: Fall through to :rethrow


:rethrow  errCode  errMsg  errLoc
setlocal disableDelayedExpansion
if not defined exception.Restart set "exception.Stack=[%~1:%~2] %exception.Stack%"
for /f "delims=" %%1 in ("%~1") do for /f "delims=" %%2 in ("%~2") do for /f "delims=" %%3 in ("%~3") do (
  setlocal enableDelayedExpansion
  for /l %%# in (1 1 10) do for /f "delims=" %%S in (" !exception.Stack!") do (
    (goto) 2>NUL
    setlocal enableDelayedExpansion
    if "!!" equ "" (
      endlocal
      setlocal disableDelayedExpansion
      call set "funcName=%%~0"
      call set "batName=%%~f0"
      if defined exception.Restart (set "exception.Restart=") else call set "exception.Stack=%%funcName%%%%S"
      setlocal EnableDelayedExpansion
      if !exception.Try! == !batName!:!funcName! (
        endlocal
        endlocal
        set "exception.Code=%%1"
        if "!!" equ "" (
          call "%~f0" setDelayed
        ) else (
          set "exception.Msg=%%2"
          set "exception.Loc=%%3"
          set "exception.Stack=%%S"
        )
        set "exception.Try="
        (CALL )
        goto :@Catch
      )
    ) else (
      for %%V in (Code Msg Loc Stack Try Restart) do set "exception.%%V="
      if "^!^" equ "^!" (
        call "%~f0" showDelayed
      ) else (
        echo(
        echo Unhandled batch exception:
        echo   Code = %%1
        echo   Msg  = %%2
        echo   Loc  = %%3
        echo   Stack=%%S
      )
      echo on
      call "%~f0" Kill
    )>&2
  )
  set exception.Restart=1
  setlocal disableDelayedExpansion
  call "%~f0" rethrow %1 %2 %3
)
:: Never reaches here


:init
set "@Try=call set exception.Try=%%~f0:%%~0"
set "@EndTry=set "exception.Try=" & goto :@endCatch"
:: Fall through to :clear


:clear
for %%V in (Code Msg Loc Stack Restart Try) do set "exception.%%V="
exit /b


:Kill - Cease all processing, ignoring any remaining cached commands
setlocal disableDelayedExpansion
if not exist "%temp%\Kill.Yes" call :buildYes
call :CtrlC <"%temp%\Kill.Yes" 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 Kill.Yes >nul
for /f "delims=(/ tokens=2" %%Y in (
  '"copy /-y nul Kill.Yes <nul"'
) do if not defined yes set "yes=%%Y"
echo %yes%>Kill.Yes
popd
exit /b


:setDelayed
setLocal disableDelayedExpansion
for %%. in (.) do (
  set "v2=%%2"
  set "v3=%%3"
  set "vS=%%S"
)
(
  endlocal
  set "exception.Msg=%v2:!=^!%"
  set "exception.Loc=%v3:!=^!%"
  set "exception.Stack=%vS:!=^!%"
)
exit /b


:showDelayed -
setLocal disableDelayedExpansion
for %%. in (.) do (
  set "v2=%%2"
  set "v3=%%3"
  set "vS=%%S"
)
for /f "delims=" %%2 in ("%v2:!=^!%") do for /f "delims=" %%3 in ("%v3:!=^!%") do for /f "delims=" %%S in ("%vS:!=^!%") do (
  endlocal
  echo(
  echo Unhandled batch exception:
  echo   Code = %%1
  echo   Msg  = %%2
  echo   Loc  = %%3
  echo   Stack=%%S
)
exit /b


:-?
:help
setlocal disableDelayedExpansion
for /f "delims=:" %%N in ('findstr /rbn ":::DOCUMENTATION:::" "%~f0"') do set "skip=%%N"
for /f "skip=%skip% tokens=1* delims=:" %%A in ('findstr /n "^" "%~f0"') do echo(%%B
exit /b


:-??
:pagedHelp
setlocal disableDelayedExpansion
for /f "delims=:" %%N in ('findstr /rbn ":::DOCUMENTATION:::" "%~f0"') do set "skip=%%N"
((for /f "skip=%skip% tokens=1* delims=:" %%A in ('findstr /n "^" "%~f0"') do @echo(%%B)|more /e) 2>nul
exit /b


:-v
:/v
:version
echo(
for /f "delims=:" %%A in ('findstr "^::EXCEPTION.BAT" "%~f0"') do echo %%A
exit /b


:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::DOCUMENTATION:::

EXCEPTION.BAT is a pure batch script utility that provides robust exception
handling within batch scripts. It enables code to be placed in TRY/CATCH blocks.
If no exception is thrown, then only code within the TRY block is executed.
If an exception is thrown, the batch CALL stack is popped repeatedly until it
reaches an active TRY block, at which point control is passed to the associated
CATCH block and normal processing resumes from that point. Code within a CATCH
block is ignored unless an exception is thrown.

An exception may be caught in a different script from where it was thrown.

If no active TRY is found after throwing an exception, then an unhandled
exception message is printed to stderr, all processing is terminated within the
current CMD Shell, and control is returned to the Shell command line.

TRY blocks are specified using macros. Obviously the macros must be defined
before they can be used. The TRY macros are defined using the following CALL

    call exception init

Besides defining @Try and @EndTry, the init routine also explicitly clears any
residual exception that may have been left by prior processing.

A TRY/CATCH block is structured as follows:

    %@Try%
      REM any normal code goes here
    %@EndTry%
    :@Catch
      REM exception handling code goes here
    :@EndCatch

- Every TRY must have an associated CATCH.

- TRY/CATCH blocks cannot be nested.

- Any script or :labeled routine that uses TRY/CATCH must have at least one
  SETLOCAL prior to the appearance of the first TRY.

- TRY/CATCH blocks use labels, so they should not be placed within parentheses.
  It can be done, but the parentheses block is broken when control is passed to
  the :@Catch or :@EndCatch label, and the code becomes difficult to interpret
  and maintain.

- Any valid code can be used within a TRY or CATCH block, including CALL, GOTO,
  :labels, and balanced parentheses. However, GOTO cannot be used to leave a
  TRY block. GOTO can only be used within a TRY block if the label appears
  within the same TRY block.

- GOTO must never transfer control from outside TRY/CATCH to within a TRY or
  CATCH block.

- CALL should not be used to call a label within a TRY or CATCH block.

- CALLed routines containing TRY/CATCH must have labels that are unique within
  the script. This is generally good batch programming practice anyway.
  It is OK for different scripts to share :label names.

- If a script or routine recursively CALLs itself and contains TRY/CATCH, then
  it must not throw an exception until after execution of the first %@Try%

Exceptions are thrown by using

    call exception throw  Code  Message  Location

where

    Code = The numeric code value for the exception.

    Message = A description of the exception.

    Location = A string that helps identify where the exception occurred.
               Any value may be used. A good generic value is "%~f0[%~0]",
               which expands to the full path of the currently executing
               script, followed by the currently executing routine name
               within square brackets.

The Message and Location values must be quoted if they contain spaces or poison
characters like & | < >. The values must not contain additional internal quotes,
and they must not contain a caret ^.

The following variables will be defined for use by the CATCH block:

  exception.Code  = the Code value
  exception.Msg   = the Message value
  exception.Loc   = the Location value
  exception.Stack = traces the call stack from the CATCH block (or command line
                    if not caught), all the way to the exception.

If the exception is not caught, then all four values are printed as part of the
"unhandled exception" message, and the exception variables are not defined.

A CATCH block should always do ONE of the following at the end:

- If the exception has been handled and processing can continue, then clear the
  exception definition by using

    call exception clear

  Clear should never be used within a Try block.

- If the exception has not been fully handled, then a new exception should be
  thrown which can be caught by a higher level CATCH. You can throw a new
  exception using the normal THROW, which will clear exception.Stack and any
  higher CATCH will have no awareness of the original exception.

  Alternatively, you may rethrow an exception and preserve the exeption stack
  all the way to the original exception:

    call exception rethrow  Code  Message  Location

  It is your choice as to whether you want to pass the original Code and/or
  Message and/or Location. Either way, the stack will preserve all exceptions
  if rethrow is used.

  Rethrow should only be used within a CATCH block.


One last restriction - the full path to EXCEPTION.BAT must not include ! or ^.


This documentation can be accessed via the following commands

    constant stream:   exception /?   OR  exception help
    paged via MORE:    exception /??  OR  exception pagedHelp

The version of this utility can be accessed via

    exception /v  OR  exception version


EXCEPTION.BAT was designed and written by Dave Benham, with important
contributions from DosTips users jeb and siberia-man.

Development history can be traced at:
  http://www.dostips.com/forum/viewtopic.php?f=3&t=6497

以下は、EXCEPTION.BATの機能をテストするスクリプトです。スクリプトは、自身を7回再帰的に呼び出します。各反復には2つのCALLがあり、1つは通常の例外伝播を示す:labelに対するものであり、もう1つはスクリプトCALL全体の例外伝播を示すスクリプトに対するものです。

再帰呼び出しから戻るときに、反復カウントが3の倍数(反復3および6)の場合、例外をスローします。

各CALLには、通常は例外を報告し、変更された例外を再スローする独自の例外ハンドラーがあります。ただし、反復カウントが5の場合、例外は処理され、通常の処理が再開されます。

@echo off

:: Main
setlocal enableDelayedExpansion
if not defined @Try call exception init

set /a cnt+=1
echo Main Iteration %cnt% - Calling :Sub
%@Try%
(
  call :Sub
  call echo Main Iteration %cnt% - :Sub returned %%errorlevel%%
)
%@EndTry%
:@Catch
  setlocal enableDelayedExpansion
  echo(
  echo Main Iteration %cnt% - Exception detected:
  echo   Code     = !exception.code!
  echo   Message  = !exception.msg!
  echo   Location = !exception.loc!
  echo Rethrowing modified exception
  echo(
  endlocal
  call exception rethrow -%cnt% "Main Exception^!" "%~f0<%~0>"
:@EndCatch
echo Main Iteration %cnt% - Exit
exit /b %cnt%


:Sub
setlocal
echo :Sub Iteration %cnt% - Start
%@Try%
  if %cnt% lss 7 (
    echo :Sub Iteration %cnt% - Calling "%~f0"
    call "%~f0"
    %= Show any non-exception return code (demonstrate ERRORLEVEL is preserved if no exception) =%
    call echo :Sub Iteration %cnt% - testException returned %%errorlevel%%
  )
  %= Throw an exception if the iteration count is a multiple of 3 =%
  set /a "1/(cnt%%3)" 2>nul || (
    echo Throwing exception
    call exception throw -%cnt% "Divide by 0 exception^!" "%~f0<%~0>"
  )
%@EndTry%
:@Catch
  setlocal enableDelayedExpansion
  echo(
  echo :Sub Iteration %cnt% - Exception detected:
  echo   Code     = !exception.code!
  echo   Message  = !exception.msg!
  echo   Location = !exception.loc!
  endlocal
  %= Handle the exception if iteration count is a multiple of 5, else rethrow it with new properties =%
  set /a "1/(cnt%%5)" 2>nul && (
    echo Rethrowing modified exception
    echo(
    call exception rethrow -%cnt% ":Sub Exception^!" "%~f0<%~0>"
  ) || (
    call exception clear
    echo Exception handled
    echo(
  )
:@EndCatch
echo :Sub Iteration %cnt% - Exit
exit /b %cnt%

-出力-

Main Iteration 1 - Calling :Sub
:Sub Iteration 1 - Start
:Sub Iteration 1 - Calling "C:\test\testException.bat"
Main Iteration 2 - Calling :Sub
:Sub Iteration 2 - Start
:Sub Iteration 2 - Calling "C:\test\testException.bat"
Main Iteration 3 - Calling :Sub
:Sub Iteration 3 - Start
:Sub Iteration 3 - Calling "C:\test\testException.bat"
Main Iteration 4 - Calling :Sub
:Sub Iteration 4 - Start
:Sub Iteration 4 - Calling "C:\test\testException.bat"
Main Iteration 5 - Calling :Sub
:Sub Iteration 5 - Start
:Sub Iteration 5 - Calling "C:\test\testException.bat"
Main Iteration 6 - Calling :Sub
:Sub Iteration 6 - Start
:Sub Iteration 6 - Calling "C:\test\testException.bat"
Main Iteration 7 - Calling :Sub
:Sub Iteration 7 - Start
:Sub Iteration 7 - Exit
Main Iteration 7 - :Sub returned 7
Main Iteration 7 - Exit
:Sub Iteration 6 - testException returned 7
Throwing exception

:Sub Iteration 6 - Exception detected:
  Code     = -6
  Message  = Divide by 0 exception!
  Location = C:\test\testException.bat<:Sub>
Rethrowing modified exception


Main Iteration 6 - Exception detected:
  Code     = -6
  Message  = :Sub Exception!
  Location = C:\test\testException.bat<:Sub>
Rethrowing modified exception


:Sub Iteration 5 - Exception detected:
  Code     = -6
  Message  = Main Exception!
  Location = C:\test\testException.bat<C:\test\testException.bat>
Exception handled

:Sub Iteration 5 - Exit
Main Iteration 5 - :Sub returned 5
Main Iteration 5 - Exit
:Sub Iteration 4 - testException returned 5
:Sub Iteration 4 - Exit
Main Iteration 4 - :Sub returned 4
Main Iteration 4 - Exit
:Sub Iteration 3 - testException returned 4
Throwing exception

:Sub Iteration 3 - Exception detected:
  Code     = -3
  Message  = Divide by 0 exception!
  Location = C:\test\testException.bat<:Sub>
Rethrowing modified exception


Main Iteration 3 - Exception detected:
  Code     = -3
  Message  = :Sub Exception!
  Location = C:\test\testException.bat<:Sub>
Rethrowing modified exception


:Sub Iteration 2 - Exception detected:
  Code     = -3
  Message  = Main Exception!
  Location = C:\test\testException.bat<C:\test\testException.bat>
Rethrowing modified exception


Main Iteration 2 - Exception detected:
  Code     = -2
  Message  = :Sub Exception!
  Location = C:\test\testException.bat<:Sub>
Rethrowing modified exception


:Sub Iteration 1 - Exception detected:
  Code     = -2
  Message  = Main Exception!
  Location = C:\test\testException.bat<C:\test\testException.bat>
Rethrowing modified exception


Main Iteration 1 - Exception detected:
  Code     = -1
  Message  = :Sub Exception!
  Location = C:\test\testException.bat<:Sub>
Rethrowing modified exception


Unhandled batch exception:
  Code = -1
  Msg  = Main Exception!
  Loc  = C:\test\testException.bat<testException>
  Stack= testException [-1:Main Exception!]  :Sub [-1::Sub Exception!]  C:\test\testException.bat [-2:Main Exception!]  :Sub [-2::Sub Exception!]  C:\test\testException.bat [-3:Main Exception!]  :Sub [-3::Sub Exception!]  [-3:Divide by 0 exception!]

最後に、中間スクリプトがそれらについて何も知らない場合でも例外を効果的に使用する方法を示す一連の簡単なスクリプトを示します。

2つの数値を除算して結果を出力する単純な除算スクリプトユーティリティから始めます。

divide.bat

:: divide.bat  numerator  divisor
@echo off
setlocal
set /a result=%1 / %2 2>nul || call exception throw -100 "Division exception" "divide.bat"
echo %1 / %2 = %result%
exit /b

エラーを検出した場合にスクリプトが例外をスローする方法に注意してください。ただし、例外をキャッチするためには何もしません。

ここで、バッチ例外についてまったく単純な除算テストハーネスを作成します。

testDivide.bat

@echo off
for /l %%N in (4 -1 0) do call divide 12 %%N
echo Finished successfully!

- 出力 -

C:\test>testDivide
12 / 4 = 3
12 / 3 = 4
12 / 2 = 6
12 / 1 = 12

Unhandled batch exception:
  Code = -100
  Msg  = Division exception
  Loc  = divide.bat
  Stack= testDivide divide [-100:Division exception]

Divide.batによって発生した例外が処理されなかったため、最後のECHOが実行されないことに注意してください。

最後に、単純なtestDivideを呼び出し、例外を適切に処理するマスタースクリプトを作成します。

master.bat

@echo off
setlocal
call exception init

%@Try%
  call testDivide
%@EndTry%
:@Catch
  echo %exception.Msg% detected and handled
  call exception clear
:@EndCatch
echo Finished Successfully!

-出力-

C:\test>master
12 / 4 = 3
12 / 3 = 4
12 / 2 = 6
12 / 1 = 12
Division exception detected and handled
Finished Successfully!

マスタースクリプトは、例外について何も知らないtestDivide.batを通過する必要があったにもかかわらず、divide.batによって発生した例外を正常にキャッチできました。とてもかっこいい :-)

これは間違いなく、エラー処理に関連するすべてのことに対する万能薬ではありません。

  • 組み込みのドキュメントで完全に説明されている構文およびコードレイアウトの制限がいくつかあります。しかし、あまりにもひどいものはありません。

  • すべてのエラーを例外として自動的に処理する方法はありません。すべての例外は、コードによって明示的にスローされる必要があります。エラー報告は慣例により処理されることを考えると、これはおそらく良いことです-厳密な規則はありません。一部のプログラムは規則に従っていません。たとえば、HELP ValidCommandはERRORLEVEL 1を返します。これは慣例によりエラーを意味し、HELP InvalidCommandはERRORLEVEL 0を返します。これは成功を意味します。

  • このバッチ例外手法では、致命的な実行時エラーをキャッチして処理することはできません。たとえば、GOTO :NonExistentLabelは、エラーをキャッチする機会なしに、すべてのバッチ処理を即座に終了します。

http://www.dostips.com/forum/viewtopic.php?f=3&t=6497 でEXCEPTION.BATの開発をフォローできます。将来の開発はそこに投稿されます。 StackOverflowのこの投稿は更新しません。

30
dbenham

successCmd && ( failingCmd & (call ) ) || ( excHandlingCmd )構文を使用することは、多くの場合非常に便利であり、ここで説明する追加フ​​ァイルも必要ありません。 https://stackoverflow.com/a/17085933/191592

(ダミー(call )は、2番目(最後)のコマンドが失敗した場合のみ)

0