Coding style used for my bash projects (v3.2 Oct 2018) As bash is clearly an error prone script language, we'll use as much standard coding as possible, including some quick and dirty debug techniques described here. ++++++ Header Always use the following header ----BEGIN HEADER #!/usr/bin/env bash PROGRAM="program-name" # Long description AUTHOR="(C) 20XX-20YY by Orsiris de Jong" CONTACT="http://www.example.com me@example.com" PROGRAM_BUILD=YYYYMMDDVV ## Optional instructions ----END HEADER Using bind style versionning: YYYYMMDDVV (Year, Month, Day, Revision): Example: 2015012402 = 2nd revision of 24 Jan 2015 #!/usr/bin/env bash instead of #!/bin/bash Change old scripts with for i in $(grep -r '#!/bin/bash' * |cut -f1 -d':'); do sed -i 's&#!/bin/bash&#!/usr/bin/env bash&g' $i; done type instead of type -p for bash test (other shells don't know -p) ++++++ Indentation Using tabs Transform old shell scripts using unexpand command ++++++ Comments Some command # comment ## Some comment on a new line ################################################# Some separation ++++++ Work comments Whenever there is some idea to postpone, use #TODO(priority):[dev-name:] some remark Priority can be critical, high, medium, low, verylow. No release can happen if there are TODOs other than low or verylow. Example: #TODO(high):deajan: need to do something A "work in progress" marker must be left on the line a dev is working when it's work isn't finished). Marker is #WIP:dev-name: some remark dev-name is mandatory if more than one person is coding Example: #WIP:deajan: missing function something ++++++ Variables All local variables names have each first letter of the words uppercase and all others lowercase, except for the first word where all letters are lowercase Example: someLongVariable All global variables are full upercase, separated by _ Example: EXEC_TIME All environment variables (verbose, silent, debug, etc) have prefix _ and are full upercase, separated by _ Example: _PARANOIA_DEBUG Exec time variables that can take boolean values should use true and false instead of 1 and 0. ++++++ Functions All function names should begin with an uppercase letter for every word, the other letters should be lowercase Example: SomeFunctionThatRocks Bash does not provide any checks against missing function arguments. Also, missing quotes can lead to an inconsistent number of arguments. Most functions should have a first line that calls the special function __CheckArguments, which checks the number of given arguments for a function in order to find possible problems. Number of arguments are given as first argument to __CheckArguments. May be a number or a range, eg 0-2 if the function takes optional arguments. __CheckArguments will only trigger when the script is launched with _PARANOIA_DEBUG=yes. Also, it will only exist in the debug version. Use the following convention for function definition: function SomeFunction { __CheckArguments 0 $# "$@" #__WITH_PARANOIA_DEBUG ... } Use sed ':a;N;$!ba;s/\n{\n/ {\n/g' to convert functions that have opening brackets on a new line. If the function has arguments, use local variable names that are more readable than $1...$n. Explain via comments what those variables contain if needed. Declare arguments before launching __CheckArguments: function AnotherFunction { local varName="${1}" local otherVarName="${2}" # This variable contains stuff __CheckArguments 2 $# "$@" #__WITH_PARANOIA_DEBUG ... } Functions should always have return status function RandomFunction { ... return $? } ++++++ Sub functions When a function is a subroutine of another function, it is called _SomethingAsSubFunction: Example: function _ApplyLocally function _ApplyRemotely function Apply ++++++ For and While statements For and while statements will have the "do" part on the first line. Example: for i in "${var[@]}"; do ... done while [ $i -eq 1 ]; do ... done ++++++ If statements If statements will be fully written (word "if" must be used). then is written on the same line. (Use sed ':a;N;$!ba;s/]\n\t*then/]; then/g' to convert files to this format... Replace "],new line, zero or more tabs, then" by "; then") if [ something ]; then stuff else other stuff fi ++++++ Logging A logging function is available that writes both to log file and stdout/stderr. It has the following global variable modifiers: _LOGGER_SILENT=true/false: disables any output to stdout/stderr _LOGGER_VERBOSE=true/false: logs messages with log level VERBOSE _LOGGER_ERR_ONLY=true/false: disables logging to log file and stdout/stderr except for CRITICAL, ERROR, WARN and ALWAYS log levels. The following log levels exist: - PARANOIA_DEBUG: Only used by debugging functions themselves - DEBUG: Only log this when _DEBUG flag is set in program. Any command forged for eval instruction should be logged by this. - NOTICE: Standard messages - ALWAYS: Standard messages, regardless of _LOGGER_ERR_ONLY - WARN: Requires attention - ERROR: Program produced an error but continues execution - CRITICAL: Program execution is halted Can be called with: Logger "My message" "LOGLEVEL" $retval $retval is an optional parameter that passes the exit code of the command that triggered the logging message $retval, along with function stack, script pid and current pid can be found in the ERROR /WARN alert files ($RUN_DIR/$PROGRAM.Logger.error.$SCRIPT_PID) ++++++ Eval Most commands should be logged to a tmp file. The basic way of doing is: cmd='"something '$somevar'" > some_file 2>&1' eval $cmd & WaitForTaskCompletion $! 0 0 1 0 true false true false retval=$? if [ $retval -ne 0 ]; then Logger "Some error message" "ERROR" $retval fi ++++++ includes Using merge.sh, the program may have includes like include #### RemoteLogger SUBSET #### All possible includes are listed in ofunctions.sh Mostly, includes are needed to port functions to a remote shell without writing them again. ++++++ Remote execution Remote commands should always invoke bash (using '"'"' to escape single quotes of 'bash -c "command"'). It is preferable to use ssh heredoc in order to use plain code. If local and remote code is identical, wrap remote code in a function so only minor modifications are needed. Remote code return code is transmitted via exit. cmd=$SSH_CMD' '"'"'bash -c "some; commands \"'$VARIABLE'\" some; other; commands" > some_file'"'"' 2>&1' Better formule $SSH_CMD remoteVar="'$localVar'" 'bash -s' << 'ENDSSH' > 2>&1 function remoteSub { some code return 0 } remoteSub exit $? ENDSSH retval=$? if [ $retval -ne 0 ]; then Logger "Some error message" "ERROR" $retval fi We also need to transmit a couple of environment variables (RUN_DIR; PROGRAM; _LOGGER_VERBOSE... see current setups) in order to make standard code. Include works here too. ++++++ File variables All eval cmd should exit their content to a file called "$RUNDIR/$PROGRAM.${FUNCNAME[0]}.$SCRIPT_PID" Dots are used instead of '_' so variables can be separated with a forbidden char in variable names, so the separtors apply as wished. ++++++ String function calls String returning functions should only be called this way in order to deal with spaces: Quoting happens outside the function call. echo "$(myStringFunction $myStringVar)" ++++++ ofunctions As obackup and osync share alot of common functions, ofunctions.sh will host all shared code. Dev programs n_osync.sh and n_obackup.sh will source ofunctions.sh Release programs will still include ofunctions.sh in order to enhance ease of use. Ofunctions are defined like: #__FUNC:FunctionName function FunctionName { } #__ENDFUNC These functions are inserted into code that has placeholders like #__FUNC:FuncName +++++++ includes ofunctions parts can be directly included in shell scripts. The parts that needs to be included must be contained within specific comments: #### MyFunction SUBSET #### function MyFunction { ... } #### MyFunction SUBSET END #### These can later be included in shell scripts with: include #### MyFunction SUBSET #### In order to have those includes parsed, we use bootstrap.sh to launch the original shell script. Original shell script will not work because include is not a bash statement. Include the following code into original shell script include #### _OFUNCTIONS_BOOTSTRAP SUBSET #### [ "$_OFUNCTIONS_BOOTSTRAP" != true ] && echo "Please use bootstrap.sh to load this dev version of $(basename $0)" && exit 1 +++++++ Exit codes Normal exit code = 0 Run with errors exit code = 1 Run with warnings exit code = 2 Wrong shell exit code = 127 Usage function exit code = 128 +++++++ Detailled debugging When launching the program with 'bash -x', add SLEEP_TIME=1 so wait functions won't spam output Ex: SLEEP_TIME=1 bash -x ./program.sh ++++++ Finding code errors Before every release, shellcheck must be run Also a grep -Eri "TODO|WIP" osync/* must be run in order to find potential release blockers Use shellcheck.net now and then (ignore SC2086 in our case) Use a low tech approach to find uneven number of quotes per line tr -cd "'\n" < my_bash_file.sh | awk 'length%2==1 {print NR, $0}' tr -cd "\"\n" < my_bash_file.sh | awk 'length%2==1 {print NR, $0}'