bash script best practice - ghdrako/doc_snipets GitHub Wiki

Shell frameworks:

Guide

  • Shell Style Guide
  • Bash Style Guide
  • Bash best practices

Tools

# sprawdzenie zaleznosci
if ! [ -x "$(command -v jq)" ]
then
  echo " Polecenie jq jest niedostepne" >&2
  exit 1
fi
  1. Shebang
#!/usr/bin/env bash
  1. Fail fast https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/

Normally Bash does not care if some command failed, returning a non-zero exit status code.

set -Eeuo pipefail
  • -e or -o errexit - option will cause a bash script to exit immediately when a command fails. you can append a command with || true for those rare cases where you don’t want a failing command to trigger an immediate exit.
#!/bin/bash
set -e

foo || true
$(ls foobar) || true
echo "bar"

# output
# ------
# line 4: foo: command not found
# ls: foobar: No such file or directory
# bar

Failing commands in a conditional statement will not cause an immediate exit

#!/bin/bash
set -e

# we make 'ls' exit with exit code 1 by giving it a nonsensical param
if ls foobar; then
  echo "foo"
else
  echo "bar"
fi

# output
# ------
# ls: foobar: No such file or directory
# bar
  • -o pipefail - The bash shell normally only looks at the exit code of the last command of a pipeline. It causes the -e option to only be able to act on the exit code of a pipeline’s last command. This particular option sets the exit code of a pipeline to that of the rightmost command to exit with a non-zero status, or to zero if all commands of the pipeline exit successfully.

  • -u or -o nounset - This option causes the bash shell to treat unset variables as an error and exit immediately.

  • -x or -o xtrace causes bash to print each command before executing it. This can be a great help when trying to debug a bash script failure.

  • -E -e without -E will cause an ERR trap to not fire in certain scenarios

  1. Try to clean up
trap cleanup SIGINT SIGTERM ERR EXIT

cleanup() {
  trap - SIGINT SIGTERM ERR EXIT
  # script cleanup here
}
  1. Parse any parameters
parse_params() {
  # default values of variables set from params
  flag=0
  param=''

  while :; do
    case "${1-}" in
    -h | --help) usage ;;
    -v | --verbose) set -x ;;
    --no-color) NO_COLOR=1 ;;
    -f | --flag) flag=1 ;; # example flag
    -p | --param) # example named parameter
      param="${2-}"
      shift
      ;;
    -?*) die "Unknown option: $1" ;;
    *) break ;;
    esac
    shift
  done

  args=("$@")

  # check required params and arguments
  [[ -z "${param-}" ]] && die "Missing required parameter: param"
  [[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"

  return 0
}
# Parse options
while [ $# -gt 0 ]; do
    case "$1" in
        -k | --keep)
            PGBACKUP_KEEP=$2; shift 2;;
        --keep=*)
            PGBACKUP_KEEP="${1#*=}"; shift 1;;

        -w | --password)
            PGBACKUP_PASSWORD=$2; shift 2;;
        --password=*)
            PGBACKUP_PASSWORD="${1#*=}"; shift 1;;

        -W | --password-file)
            PGBACKUP_PASSWORD_FILE=$2; shift 2;;
        --password-file=*)
            PGBACKUP_PASSWORD_FILE="${1#*=}"; shift 1;;

        -d | --dest | --destination)
            PGBACKUP_DESTINATION=$2; shift 2;;
        --dest=* | --destination=*)
            PGBACKUP_DESTINATION="${1#*=}"; shift 1;;

        -c | --compress | --level)
            PGBACKUP_COMPRESS=$2; shift 2;;
        --compress=* | --level=*)
            PGBACKUP_COMPRESS="${1#*=}"; shift 1;;

        -t | --then)
            PGBACKUP_THEN=$2; shift 2;;
        --then=*)
            PGBACKUP_THEN="${1#*=}"; shift 1;;

        -v | --verbose)
            PGBACKUP_VERBOSE=1; shift 1;;

        -\? | --help)
            usage 0;;
        --)
            shift; break;;
        -*)
            echo "Unknown option: $1 !" >&2 ; usage 1;;
        *)
            break;;
    esac
done
##
# Color Variables
##
green='\e[32m'
blue='\e[34m'
clear='\e[0m'
##
# Color Functions
##
ColorGreen(){
  echo -ne $green$1$clear
}
ColorBlue(){
  echo -ne $blue$1$clear
}
menu(){
echo -ne "
My First Menu
$(ColorGreen '1)') Memory usage
$(ColorGreen '2)') CPU load
$(ColorGreen '3)') Number of TCP connections
$(ColorGreen '4)') Kernel version
$(ColorGreen '5)') Check All
$(ColorGreen '0)') Exit
$(ColorBlue 'Choose an option:') "
read a
case $a in
  1) memory_check ; menu ;;
  2) cpu_check ; menu ;;
  3) tcp_check ; menu ;;
  4) kernel_check ; menu ;;
  5) all_checks ; menu ;;
  0) exit 0 ;;
  *) echo -e $red"Wrong option."$clear;
  WrongCommand;;
esac
}

menu
  1. Check that arguments are of the correct type Easy way to check if an argument is numeric
if ! [ "$1" -eq "$1" 2> /dev/null ]
then
  echo "ERROR: $1 is not a number!"
  exit 1
fi
  1. Display helpful help
usage() {
  cat << EOF # remove the space between << and EOF, this is due to web plugin issue
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]

Script description here.

...
EOF
  exit
}

# Dynamic vars
cmdname=$(basename "$(readlink -f "$0")")
appname=${cmdname%.*}

# Print usage on stderr and exit
usage() {
  exitcode="$1"
  cat << USAGE >&2

Description:
  $cmdname will compress the latest file matching a pattern, compress it,
  move it to a destination directory and rotate files in this directory
  to keep disk space under control. Compression via zip is preferred,
  otherwise gzip.

Usage:
  $cmdname [-option arg] pattern

  where all dash-led single options are as follows:
    -v              Be more verbose
    -d destination  Directory where to place (and rotate) compressed copies, default to current dir
    -k keep         Number of compressed copies to keep, defaults to empty, meaning all
    -c level        Compression level, defaults to 0, meaning no compression
    -w password     Password for compressed archive, only when zip available
    -W path         Same as -w, but read content of password from file instead
    -t command      Command to execute once done, path to copy will be passed as an argument
USAGE
  exit "$exitcode"
}
  1. ShellCheck linter Install
sudo apt install shellcheck

On EPEL based distros:

sudo yum -y install epel-release
sudo yum install ShellCheck

On Fedora based distros:

dnf install ShellCheck

Integrate with editor:

Using:

$>shellcheck script.sh
  • disable
# shellcheck disable=code[,code...]
statement_where_warning_should_be_disabled
# shellcheck disable=SC2012,SC2068
LATEST=$(ls -1 $@ 2>/dev/null | sort | tail -n 1)
  • enable
#!/bin/bash
# shellcheck enable=require-variable-braces
echo "Hello $USER"  # Will suggest ${USER}

list optional checks

shellcheck --list-optional
  1. Use template
  1. Check if needed files actually exist
if [ ! -f $1 ]; then
    echo "$1 -- no such file"
fi
  1. Variable Annotations Bash allows for a limited form of variable annotations. The most important ones are:
  • local (for local variables inside a function)
  • readonly (for read-only variables)
  1. Always use $(cmd) for command substitution (as opposed to backquotes)
  2. Prefer using Double Brackets
  3. Avoiding Temporary Files Some commands expect filenames as parameters so straightforward pipelining does not work. This is where <() operator comes in handy as it takes a command and transforms it into something which can be used as a filename:
diff <(wget -O - url1) <(wget -O - url2)
  1. Debugging
bash -n myscript.sh  # perform a syntax check/dry run of your bash script
bash -v myscripts.sh # produce a trace of every command executed or set -o verbose
bash -x myscript.sh  # produce a trace of the expanded command or set -o xtrace
  1. Logging
  • 0: /dev/stdin
  • 1: /dev/stdout
  • 2: /dev/stderr You can redirect these so that, for example, stdout can go to a file:
exec 1>log

Now, anything written to the stdout file descriptor within the current shell (and all sub-shells) goes to the file log.

Two equivalent ways of writing from /dev/stdout to /dev/stderr.

command 1>&2
command >&2

Simple logger

! /bin/bash

#LOG FILE location
log_file=/tmp/mylogfile.log

#the LOG function
LOG()
{
time=$(date '+%Y-%m-%d %H:%M:%S')
echo "$time"" >>> "$1 >>${log_file}
}

message= echo "test logger message"
#to send loggers to your log file use
LOG "my message logged to my log file with timestamp = ""$message" 
  1. printf is preferable to echo
error() {
    printf "${red}!!! %s${reset}\\n" "${*}" 1>&2
  }
# Colourisation support for logging and output.
_colour() {
    if [ "$INTERACTIVE" = "1" ]; then
        # shellcheck disable=SC2086
        printf '\033[1;31;'${1}'m%b\033[0m' "$2"
    else
        printf -- "%b" "$2"
    fi
}
green() { _colour "32" "$1"; }
red() { _colour "40" "$1"; }
yellow() { _colour "33" "$1"; }
blue() { _colour "34" "$1"; }

# Conditional logging
log() {
    if [ "$PGBACKUP_VERBOSE" = "1" ]; then
        echo "[$(blue "$appname")] [$(yellow info)] [$(date +'%Y%m%d-%H%M%S')] $1" >&2
    fi
}

warn() {
    echo "[$(blue "$appname")] [$(red WARN)] [$(date +'%Y%m%d-%H%M%S')] $1" >&2
}

  1. Use function
foo() {
    local first_arg="${1}"
    local second_arg="${2}"
    [...]
  }
  1. Template
#! /usr/bin/env bash
#
# Author: 
#
#/ Usage: SCRIPTNAME [OPTIONS]... [ARGUMENTS]...
#/
#/ 
#/ OPTIONS
#/   -h, --help
#/                Print this help message
#/
#/ EXAMPLES
#/  


#{{{ Bash settings
# abort on nonzero exitstatus
set -o errexit
# abort on unbound variable
set -o nounset
# don't hide errors within pipes
set -o pipefail
#}}}
#{{{ Variables
IFS=$'\t\n'   # Split on newlines and tabs (but not on spaces)
script_name=$(basename "${0}")
script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
readonly script_name script_dir

#}}}

main() {
  # check_args "${@}"
  :
}

#{{{ Helper functions



#}}}

main "${@}"

  1. Create temporary directory or file
TMPDIR=$(mktemp -d -t offline.XXXXXX)  # -d to create dir -t use named template
#!/usr/bin/env sh

for f in 1 2 3
do
  file=$(mktemp)
  echo "Writing file $file"
  echo "My Contents" >> $file
done

Use named template

#!/bin/bash

for f in 1 2 3
do
  file=$(mktemp -t file_${f})
  echo "Writing file $file"
  echo "My Contents" >> $file
done

Create dir

$TMPDIR=$(mktemp -d)
# do something in tempdir
# Cleanup temporary directory
rm -rf "$TMPDIR"
  1. Check command existance
ZEXT=
COMPRESSOR=
if [ "$COMPRESS_LEVEL" -gt "0" ]; then
    ZIP=$(command -v zip)
    if [ -n "$ZIP" ]; then
        ZEXT="zip"
        COMPRESSOR=$ZIP
    else
        GZIP=$(command -v gzip)
        if [ -n "$GZIP" ]; then
            ZEXT="gz"
            COMPRESSOR=$GZIP
        fiVERBOSE=${PGBACKUP_VERBOSE:-0}
    fi
    if [ -n "$COMPRESSOR" ]; then
        log "Will use $COMPRESSOR for compressing, extension: $ZEXT"
    else
        warn "No compression possible, could neither find zip, nor gzip binaries"
    fi
fi

  1. Default values for variable
VERBOSE=${VERBOSE:-0}
PORT=${PORT:-${POSTGRES_PORT:-5432}}
USER=${USER:-${POSTGRES_USER:-postgres}}
⚠️ **GitHub.com Fallback** ⚠️