Search code examples
batch-filelabelshared-librariesgotosubroutine

Is there any way to maintain a batch script library?


I'm writing a complex batch patch file with generation of other files. I know that batch is not the best for it but I've mostly got it all working. However, there is a headache of keeping all subroutines duplicated in all files if I don't want to make a separate file for each subroutine. My question was whether there is any way of keeping a library file of multiple subroutines and call them somehow?


Solution

  • So I'm going to answer my own question because I managed to get something working.

    Lets say you have main.bat and lib.bat files.

    Main.bat

    @ECHO off
    SETLOCAL
        SET return=
        SET reference=this is just a reference variable
    
        CALL lib.bat return "subroutine" "static arg" reference
        IF NOT "%ERRORLEVEL%"=="0" (
            ECHO Execution failed - %ERRORLEVEL%
            EXIT /b 1
        )
    
        ECHO.
        ECHO subroutine return value: "%return%"
        ECHO.
    
        CALL lib.bat NUL "procedure" "static arg" reference
    
        ECHO.
    
        CALL lib.bat return "error" "static arg" reference
        IF NOT "%ERRORLEVEL%"=="0" (
            ECHO Execution failed - %ERRORLEVEL%
            EXIT /b 1
        )
    ENDLOCAL
    EXIT /b 0
    

    Lib.bat

    @ECHO off
    
    ::
    :: ===================================================================================
    :: Library Main Handler
    :: ===================================================================================
    :: %~1  - [out] - NUL | reference to a return variable
    :: %~2  - [in]  - subroutine label to be invoked
    :: %~3+ - [in]  - optional arguments to the subroutine
    ::
    :: ERRORLEVEL is passed through to the caller
    ::
    
    SETLOCAL ENABLEDELAYEDEXPANSION ENABLEEXTENSIONS
        SET callSub=%~2
        SET return=
        SET args=
    
        IF "%callSub%"=="" (
            ECHO Subroutine label was not provided to the library. 1>&2
            EXIT /b 1
        )
    
        :buildUpArgumentList
        IF "%~3"=="" GOTO end_buildUpArgumentList
           SET args=%args% "%~3"
        SHIFT /3
        GOTO buildUpArgumentList
        :end_buildUpArgumentList
    
        IF NOT "%~1"=="NUL" (
            call:%callSub% return %args%
            IF NOT "!ERRORLEVEL!"=="0" (
                EXIT /b !ERRORLEVEL!
            )
        ) ELSE (
            call:%callSub% %args%
            IF NOT "!ERRORLEVEL!"=="0" (
                EXIT /b !ERRORLEVEL!
            )
        )
    (
        ENDLOCAL
        IF NOT "%~1"=="NUL" (
            SET %~1=%return%
        )
    )
    EXIT /b 0
    
    ::
    :: ===================================================================================
    :: Library Subroutine Definitions
    :: ===================================================================================
    ::
    
    :subroutine <r_return> <static> <r_reference>
        SETLOCAL
            ECHO subroutine^<static^>: "%~2"
            ECHO subroutine^<r_reference^>: "!%~3!"
        (
            ENDLOCAL
            SET %~1=subroutine executed OK
        )
    EXIT /b 0
    
    :procedure <static> <r_reference>
        SETLOCAL
            ECHO procedure^<static^>: "%~1"
            ECHO procedure^<r_reference^>: "!%~2!"
        ENDLOCAL
    EXIT /b 0
    
    :error <r_return> <static> <r_reference>
        SETLOCAL
            ECHO error^<static^>: "%~2"
            EXIT /b 2
            ECHO error^<r_reference^>: "!%~3!"
        (
            ENDLOCAL
            SET %~1=error executed OK
        )
    EXIT /b 0
    

    Output:

    subroutine<static>: "static arg"
    subroutine<r_reference>: "this is just a reference variable"
    
    subroutine return value: "subroutine executed OK"
    
    procedure<static>: "static arg"
    procedure<r_reference>: "this is just a reference variable"
    
    error<static>: "static arg"
    Execution failed - 2
    

    Some comments:

    • The library subroutine invocation is similar to a regular function signature: () but without parentheses.
    • You will notice that the library code checks for NUL being passed. If that is the case, return value is neither passed to the subroutine nor is it returned back.
    • The subroutine arguments support n-number of arguments.
    • The library supports error level pass-through.
    • If a provided subroutine label does not exist within the library, batch will return ERRORLEVEL=1 and a message "The system cannot find the batch label specified - [label]."

    Additional thoughts:

    • I am relatively new to batch scripting so not everything is perfect.
    • This was done in ~1hr and I'm sure its missing some stuff!
    • Should some escaping be performed when constructing the arguments array (i.e., quotes)?

    Any other comments are much appreciated!