jacob-tate
10/24/2019 - 10:46 PM

bat: Batch template

Batch template

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Template batch script                                                     ::
::                                                                           ::
:: Author: Jacob I. Tate <jacob.tate@digipen.edu>                            ::          
:: Date: 10/24/2019                                                          ::          
:: Version: 1.0.0                                                            ::          
::                                                                           ::
:: Changelog                                                                 ::
:: 1.0.0                                                                     ::
:: - Created the script                                                      ::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: NOTES:                                                                    ::
:: - To create more parameters modify the __OPTIONS variable as specified    ::
:: - To get the value from a parameter use if defined %-varname then expand  ::
::   like normal variables %-varname% or DelayedExpansion notation !-varname!::
:: - Set the log file with __LOG_FILE and enable levels there too            ::
:: - Log with call :LogLevel "My Message!"                                   ::
:: - Run commands only in debug mode via call :runDebug command param1 param2::
:: - Naming convension                                                       ::
::   - __SCREAMING_SNAKE_CASE: Constant variables which need to be removed   ::
::   - -xxx:                   Parameters                                    ::
::   - lowerCamelCase:         Functions intended for direct calls           ::
::   - lower_snake_case:       Functions intended for direct calls           ::
::   - __lower_snake_case:     Functions for internal use only               ::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

@echo off
setlocal
setlocal enableDelayedExpansion

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Script setup
call :init %*

if defined %-help (
    echo.
    echo This is the help prompt
    echo.
    call :clean_exit
)

if defined %-version (
    echo %__VERSION%
    call :clean_exit
)

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Do work!

goto :clean_exit

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: FUNCTIONS

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Initializes the script this should be the only part you need to modify    ::
::                                                                           ::
:: Variables:                                                                ::
::  [out] __NAME                                                             ::
::  [out] __VERSION                                                          ::
::  [out] __BAT_FILE                                                         ::
::  [out] __BAT_PATH                                                         ::
::  [out] __BAT_NAME                                                         ::
::  [out] __LOG_FILE                                                         ::
::  [out] __LOG_DEBUG                                                        ::
::  [out] __LOG_INFO                                                         ::
::  [out] __LOG_ERROR                                                        ::
::  [out] __OPTIONS                                                          ::
::  [out] __INTERACTIVE                                                      ::
::  [out] -xx                                                                ::
:: Usage:                                                                    :: 
::  call :init                                                               :: 
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:init
  :: Useful variables
  set "__NAME=%~n0"
  set "__VERSION=1.0.0"
  
  :: Batch file related stuff
  set "__BAT_FILE=%~n0"
  set "__BAT_PATH=%~dp0"
  set "__BAT_NAME=%~nx0"

  :: Logger related
  ::  If the log file doesnt already exist it will be created
  ::  Set the values to 1 to enable the various levels 
  set "__LOG_FILE=C:\test.log"
  set "__LOG_DEBUG="
  set "__LOG_INFO=1"
  set "__LOG_ERROR=1"

  :: Modified from https://stackoverflow.com/a/8162578
  :: Define the option names along with the default values, using a <space>
  :: delimiter between options
  ::
  :: Each option has the format -name:[default]
  :: 
  :: The option names are NOT case sensitive
  :: 
  :: Options that have a default value expect the subsequent command line to 
  :: contain the value. If the option is not provided then the option is set
  :: to the default. If the default contains spaces, special characters,
  :: or starts with a colon, then it should be enclosed with double quotes.
  :: The default can be undefined by specifying the default as empty quotes ""
  :: NOTE: The default cannot contain * or ?
  ::
  :: Options that are specified without any default value are simply flags that
  :: are either defined or undefined. All flags start out undefined by default
  :: and become defined if the option is supplied
  ::
  :: The order is no important
  ::
  set "__OPTIONS=-help: -version:"

  :: Initialization functions
  call :logInit
  call :parse_arguments %*
  
  :: Determine if the file is interactive to pause on exits if so
  ::  Interactive means they double clicked it to open it
  set "__INTERACTIVE=1"
  echo %cmdcmdline% | find /i "%__BAT_FILE%" >nul
  if not errorlevel 1 set "__INTERACTIVE=0"
exit /b 0

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Cleans up all the things we did in the script namely deleting vars        ::
:: NOTE: We assume all vars to delete are either __xx or -xx                 ::
::                                                                           ::
:: Variables:                                                                ::
::  [in/out] -xx                                                             ::
::  [in/out] __xx                                                            ::
:: Usage:                                                                    ::
::  call :cleanup                                                            ::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:cleanup
  :: Remove the internal variables
  for /f "tokens=1* delims==" %%a in ('set __ 2^>nul') do (
    set %%a=
    call :logDebug "Deleting %%a"
  )

  :: Remove the parameter variables
  for /f "tokens=1* delims==" %%a in ('set - 2^>nul') do (
    set %%a=
    call :logDebug "Deleting %%a"
  )
exit /b 0

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Parses the command line arguments creating a coresponding -xx variable    ::
::                                                                           ::
:: Variables:                                                                ::
::  [in]  __OPTIONS                                                          ::
::  [out] -xx                                                                ::
:: Usage:                                                                    :: 
::  call :parse_arguments %*                                                 :: 
::  echo %-test%                                                             ::
::  if defined %-help echo help flag detected!                               ::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:parse_arguments [args]
  :: Set the default option values
  :: Iterate over all the defaults
  for %%O in (%__OPTIONS%) do (
    REM Iterate over the tokens within the token
    REM These tokens are delimited by a colon ":"
    for /f "tokens=1,* delims=:" %%A in ("%%O") do (
      set "%%A=%%~B"
    )
  )

  :: This will output all the created default args
  call :runDebug set -

  :: Validate and store the options, one at a time, using a loop
  :: Options start at arg1
  :parse_arguments_loop
  if not "%~1"=="" (
    REM Create a tester for the argument to determine if the one we are 
    REM currently on is known or not
    set "__ARG_TEST=!__OPTIONS:*%~1:=! "
    call :logDebug "__ARG_TEST=!__ARG_TEST!"

    if "!__ARG_TEST!"=="!__OPTIONS! " (
      REM No substitution was made so this is an invalid option
      REM Currently error handling just outputs this
      REM Any other handling can also go here!
      echo Error: Invalid option %~1
    ) else if "!__ARG_TEST:~0,1!"==" " (
      REM Set the flag options using the options name
      REM The value shouldnt matter, they just need to be defined
      set "%~1=1"
    ) else (
      REM Set the option value using the option as the name
      REM Here we are disabling delayed expansion to allow for the use
      REM of ! and ^ in variables
      setlocal disableDelayedExpansion

      REM Escape the ! and ^ characters
      set "__TEMP_VAR=%~2"
      call :parse_arguments_escape_val

      setlocal enableDelayedExpansion
      
      for /f delims^=^ eol^= %%A in ("!__TEMP_VAR!") do (
        endlocal&endlocal&set "%~1=%%A" !
      ) 

      REM Preform the shift operation
      shift /1
    )

    REM Preform the shift operation
    shift /1
    goto :parse_arguments_loop
  )

  :parse_arguments_escape_val
    set "__TEMP_VAR=%__TEMP_VAR:^=^^%"
    set "__TEMP_VAR=%__TEMP_VAR:!=^!%"
  exit /b 0
exit /b 0

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Initializes the logger creating the log file if it doesnt already exist   ::
::                                                                           ::
:: Variables:                                                                ::
::  [in]  __LOG_FILE                                                         ::
:: Usage:                                                                    ::
::  call :logInit                                                            :: 
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:logInit
  :: Create the log file if it doesnt exist
  if not exist %__LOG_FILE% (
    echo %__LOG_FILE%
    type nul > %__LOG_FILE%

    :: Dirty exit is something went wrong
    if %ERRORLEVEL% NEQ 0 (
      echo Failed to create the file %__LOG_FILE%
      call :dirty_exit -1
    )
  )

  call :logDebug "Log init successful"
exit /b 0

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Logs assuming we are in info mode!                                        ::
::                                                                           ::
:: Variables:                                                                ::
::  [in]  __LOG_FILE                                                         ::
::  [in]  __LOG_INFO                                                         ::
:: Usage:                                                                    ::
::  REM Logs only if info mode is enabled                                    ::
::  set "__LOG_INFO=1"                                                       ::
::  call :logInfo "Info is enabled!"                                         ::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:logInfo [message]
  if defined %__LOG_INFO (
    echo [%date% - %time%] INFO: %~1
    echo [%date% - %time%] INFO: %~1 >> %__LOG_FILE%
  )
exit /b 0

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Logs assuming we are in error mode!                                       ::
::                                                                           ::
:: Variables:                                                                ::
::  [in]  __LOG_FILE                                                         ::
::  [in]  __LOG_ERROR                                                        ::
:: Usage:                                                                    ::
::  REM Logs only if error mode is enabled                                   ::
::  set "__LOG_ERROR=1"                                                      ::
::  call :logError "Error is enabled!"                                       ::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:logError [message]
  if defined %__LOG_ERROR (
    echo [%date% - %time%] ERROR: %~1
    echo [%date% - %time%] ERROR: %~1 >> %__LOG_FILE%
  )
exit /b 0

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Logs assuming we are in debug mode!                                       ::
::                                                                           ::
:: Variables:                                                                ::
::  [in]  __LOG_FILE                                                         ::
::  [in]  __LOG_DEBUG                                                        ::
:: Usage:                                                                    ::
::  REM Logs only if debug mode is enabled                                   ::
::  set "__LOG_DEBUG=1"                                                      ::
::  call :logDebug "Debug is enabled!"                                       ::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:logDebug [message]
  if defined %__LOG_DEBUG (
    echo [%date% - %time%] DEBUG: %~1
    echo [%date% - %time%] DEBUG: %~1 >> %__LOG_FILE%
  )
exit /b 0

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Calls the provided function assuming we are in debug mode!                ::
::                                                                           ::
:: Variables:                                                                ::
::  [in]  __LOG_FILE                                                         ::
::  [in]  __LOG_DEBUG                                                        ::
:: Usage:                                                                    ::
::  REM Runs set __ only if on debug mode!                                   ::
::  set "__LOG_DEBUG=1"                                                      ::
::  call :runDebug set __                                                    ::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:runDebug [command]
  if defined %__LOG_DEBUG (
    call :logDebug "Calling %*"
    %*
  )
exit /b 0

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Exits the program with a clean exit code (0)                              ::
::                                                                           ::
:: Usage:                                                                    :: 
::  call :clean_exit                                                         :: 
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:clean_exit
  :: Pause if it was a double click
  if _%interactive%_==_0_ pause
  endlocal

  :: Functions to close up shop
  call :cleanup
  call :__exit_batch 0
goto :eof

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Exits the program with a non clean exit code                              ::
::                                                                           ::
:: Usage:                                                                    :: 
::  call :dirty_exit -1                                                      :: 
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:dirty_exit [errorcode]
  :: Pause if it was a double click
  if _%interactive%_==_0_ pause
  endlocal

  :: Functions to close up shop
  call :cleanup
  call :__exit_batch %~1
goto :eof

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Exits the batch file with the provided exit code                          ::
:: NOTE: This exits the current file but not the caller!                     ::
::                                                                           ::
:: Usage:                                                                    ::
::  call :__exit_batch -1                                                    ::
::  call :__exit_batch 0                                                     ::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:__exit_batch [errorcode]
  set _errLevel=%~1

  :: Exit the script and remove all function calls from the call stack
  :__exit_batch_pop_stack
  (
    (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 :__exit_batch_pop_stack
    )
    (goto) 2>nul
    endlocal

    cmd /c "exit /b %_errLevel%"
  )
goto :eof