bash script best practice - ghdrako/doc_snipets GitHub Wiki
Shell frameworks:
- bashing https://github.com/xsc/bashing
- rerun https://github.com/rerun/rerun
- rr http://taarr.com
Guide
- Shell Style Guide
- Bash Style Guide
- Bash best practices
Tools
- https://www.shellcheck.net/
- https://github.com/koalaman/shellcheck - run directly in your terminal
- https://explainshell.com/
# sprawdzenie zaleznosci
if ! [ -x "$(command -v jq)" ]
then
echo " Polecenie jq jest niedostepne" >&2
exit 1
fi
- Shebang
#!/usr/bin/env bash
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
- Try to clean up
trap cleanup SIGINT SIGTERM ERR EXIT
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
# script cleanup here
}
- 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
- 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
- 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"
}
- ShellCheck linter Install
- https://github.com/koalaman/shellcheck#user-content-installing Install debian/ubuntu
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:
- Sublime, through SublimeLinter.
- VSCode, through vscode-shellcheck.
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
- Use template
- https://gist.github.com/m-radzikowski/53e0b39e9a59a1518990e76c2bff8038
- https://github.com/pforret/bashew
- https://gitlab.com/methuselah-0/bash-coding-utils.sh
- https://github.com/GhostWriters/DockSTARTer/blob/master/main.sh
- https://github.com/xshoji/bash-script-starter
- Check if needed files actually exist
if [ ! -f $1 ]; then
echo "$1 -- no such file"
fi
- 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)
- Always use $(cmd) for command substitution (as opposed to backquotes)
- Prefer using Double Brackets
- 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)
- 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
- 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"
- 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
}
- Use function
foo() {
local first_arg="${1}"
local second_arg="${2}"
[...]
}
- 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 "${@}"
- 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"
- 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
- Default values for variable
VERBOSE=${VERBOSE:-0}
PORT=${PORT:-${POSTGRES_PORT:-5432}}
USER=${USER:-${POSTGRES_USER:-postgres}}