Skip to content

Instantly share code, notes, and snippets.

@gazorby
Last active February 22, 2020 23:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gazorby/1c076ffd0f26356fbc165e9d27d1a916 to your computer and use it in GitHub Desktop.
Save gazorby/1c076ffd0f26356fbc165e9d27d1a916 to your computer and use it in GitHub Desktop.
Shell script boilerplate
#!/usr/bin/env bash
############################################################
# PRIVATE FUNCTIONS
############################################################
############################################################
# PRIVATE (better to use use shortcuts)
# Display logs
#
# Arguments :
#
# $1 (required): loglevel
# - info, warning, error, step and info will be
# displayed like this : " [colored loglevel] message "
# - title will show " ==> bold title "
# - none will only show the message
# Any other loglevel (except title and none) will be green
#
# $2 (required): where to write log message
# - out : display the log immediatly
# - exit : log will be displayed on script exit
# - persist : write to a log file
############################################################
function _log () {
local message="$3"
local loglevel="$1"
local where="$2"
local loglevel_out
if [[ $loglevel = warning ]]; then
# Yellow
loglevel_out="[$YELLOW$loglevel$NORMAL] "
elif [[ $loglevel = error ]]; then
# Red
loglevel_out="[$RED$loglevel$NORMAL] "
elif [[ $loglevel = step ]]; then
# Cyan
loglevel_out="[$CYAN$loglevel$NORMAL] "
elif [[ $loglevel = none ]]; then
loglevel_out=""
elif [[ $loglevel = title && $where != persist ]]; then
# ==> title
loglevel_out="\n$BLUE==>$NORMAL "
message="$BOLD$message$NORMAL"
elif [[ $loglevel =~ ^info|.*$ ]]; then
# Green
loglevel_out="[$GREEN$loglevel$NORMAL] "
fi
# output - [loglevel] message
if [[ $where = now && $QUIET = 0 ]]; then
# No newline if info is a step
if [[ $loglevel = step ]]; then
echo -ne "$loglevel_out$message "
else
echo -e "$loglevel_out$message"
fi
# Write logs in temp file which is read on script exit
elif [[ $where = exit && $QUIET = 0 ]]; then
# Put 2 space space before each line
tmp="$(
echo -e "$message" | sed -e '
s/^\b/________ ________/g
s/\n\b/________\n ________/g
s/________//g'
)"
echo -e "$loglevel_out\n$tmp\n" >> "$OUT_END"
fi
if [[ $LOG = 1 ]]; then
loglevel=$(sed "s/title/info/g" <<< "$loglevel")
local date="[$(date '+%Y-%m-%d %H:%M:%S')]"
touch $LOG_FILE
echo -e "$date[$loglevel]\t"${@:3}"" >> $LOG_FILE
fi
}
_lock() { flock -$1 $LOCKFD; }
_no_more_locking() { _lock u; _lock xn && rm -f $LOCKFILE; }
_prepare_locking() { eval "exec $LOCKFD>\"$LOCKFILE\""; }
function _step_success () {
if [[ $QUIET = 0 ]]; then
echo -ne "[$GREEN"OK"$NORMAL]"
fi
}
function _step_failure () {
if [[ $QUIET = 0 ]]; then
echo -ne "[$RED"FAILED"$NORMAL]"
fi
}
function _step_warning () {
if [[ $QUIET = 0 ]]; then
echo -ne "[$YELLOW"WARNING"$NORMAL]"
fi
}
function _step_passed () {
if [[ $QUIET = 0 ]]; then
echo -ne "[$YELLOW"PASSED"$NORMAL]"
fi
}
############################################################
# PUBLIC FUNCTIONS
############################################################
# obtain an exclusive lock immediately or exit
exlock_now() {
if ! _lock xn; then
on_exit error "Couldn't acquire the lock!"
exit 1
fi
}
############################################################
# Locking shortcut functions
############################################################
exlock() { _lock x; } # obtain an exclusive lock
shlock() { _lock s; } # obtain a shared lock
unlock() { _lock u; } # drop a lock
############################################################
# display script usage
#
# Arguments :
# $1 (optionnmal): Cause that showing up this help
############################################################
function usage() {
# Empty string if param 1 empty
local error=${1:-""}
echo ""
echo -e "\e[31m$error\e[0m\n"
echo -e "Usage :\n"
echo -e "\e[1m\e[33m"./"$PROGNAME".sh"\e[0m [option]"
echo -e ""
echo -e " \e[1m--debug\e[0m Run script in debug mode (All executed commands are displayed)\n"
echo -e " \e[1m--lock\e[0m Lock the script (only one instance can be executed)\n"
echo -e " \e[1m-q --quiet\e[0m No output\n"
echo -e " \e[1m--logs\e[0m Print logs\n"
echo -e " \e[1m--info\e[0m Display OS type, Version and package manager found by the script\n"
echo -e " \e[1m--strict\e[0m Run script in scrict mode (enable error when trying to expand an unset parameter)\n"
echo -e " \e[1m-h --help\e[0m Show this help \n"
}
############################################################
# Log shortcut functions
############################################################
info () { _log info now "$*"; }
error () { _log error now "$*"; }
warning () { _log warning now "$*"; }
title () { _log title now "$*"; }
on_exit () { _log "$1" exit "${@:2}"; }
############################################################
# Check dependencies
#
# Usage :
# check_dep [install | exit] (package | command-names,.../package-names,...)
#
# You can specify multiple command names or package names by
# separating them with a coma :
#
# check_dep install name1,name2/package1,package2,package3
#
# If name1 is not found, it will test name2 and so on. Same
# for packages.
#
# Arguments :
# $1 (optionnal): What to do if package isn't installed
#
# install : try to install it with the package manager
# set in $INST variable. If install failed,
# it will try to install with package
# found in $INST_FALLBACK if set
#
# exit : exit script immediatly
#
# $2* (required): command-name or command-name/package-name
#
# if command name and package name are different :
# check_dep [option] command-name/package-name
#
# if command-name and package-name are the same
# check_dep [option] command-name
############################################################
function check_dep () {
local PARAM=$1
local MISSING_DEP='true'
local name=''
local command=''
local command_exist='false'
if [[ $PARAM =~ install|exit ]]; then
shift
else
PARAM='false'
fi
local DEP=$*
for req in ${DEP}
do
if [[ $req == *"/"* ]]; then
command=${req%/*}
name=${req##*/}
else
command="$req"
name="$req"
fi
for c in ${command//,/ }; do
# Will bypass aliases
if command -v $c 2>&1 > /dev/null; then
command_exist='true'
fi
done
if [[ $command_exist = false ]]; then
MISSING_DEP='true'
if [[ $PARAM == exit ]]; then
on_exit error "$name isn't installed !"
elif [[ $PARAM == install ]]; then
if [[ -z $INST ]]; then
set_papckage_manager
fi
info "$name isn't installed, try installing using $INST";
check_superuser
for n in ${name//,/ }; do
run_as_root $INST "$n" || continue
done
if [[ $? -ne 0 ]]; then
if ! [[ $INST_FALLBACK = "" ]]; then
info "trying using $INST_FALLBACK"
for n in ${name//,/ }; do
$INST_FALLBACK "$i"
done
if [[ $? -ne 0 ]]; then
return 1
fi
fi
fi
fi
return 1
fi
done
if [[ $MISSING_DEP == true && $PARAM == exit ]]; then
exit 1
fi
return 0
}
############################################################
# Set the OS and VERSION variables
# OS example : ubuntu, fedora, opensuse etc.
# VERSION example : 18.0.4, 19.10, 29, 30 etc.
# No arguments
############################################################
function get_distro () {
if type lsb_release >/dev/null 2>&1; then
# linuxbase.org
OS=$(lsb_release -si)
VER=$(lsb_release -sr)
elif [ -f /etc/lsb-release ]; then
# For some versions of Debian/Ubuntu without lsb_release command
. /etc/lsb-release
OS=$DISTRIB_ID
VER=$DISTRIB_RELEASE
elif [ -f /etc/debian_version ]; then
# Older Debian/Ubuntu/etc.
OS=Debian
VER=$(cat /etc/debian_version)
elif [ -f /etc/os-release ]; then
# freedesktop.org and systemd
. /etc/os-release
OS=$NAME
VER=$VERSION_ID
elif [ -f /etc/SuSe-release ]; then
# Older SuSE/etc.
...
elif [ -f /etc/redhat-release ]; then
# Older Red Hat, CentOS, etc.
...
else
# Fall back to uname, e.g. "Linux <version>", also works for BSD, etc.
OS=$(uname -s)
VER=$(uname -r)
fi
}
############################################################
# Set package manager in INST variable based OS and VERSION
# No arguments
############################################################
function set_papckage_manager () {
get_distro
if ! [[ -z $OS ]]; then
# OS=$(echo "$OS" | tr '[:upper:]' '[:lower:]')
OS=$(toLowerCase $OS)
if [[ $OS == 'fedora' ]]; then
INST='dnf install -y'
REM='dnf remove -y'
elif [[ $OS == 'ubuntu' ]]; then
INST='apt-get install -y'
REM='dnf remove -y'
elif [[ $OS =~ ^.*opensuse.*$ ]]; then
INST='zypper install -y'
REM='dnf remove -y'
elif [[ $OS =~ ^.*manjarolinux.*$ ]]; then
INST='pacman -S --noconfirm'
INST_FALLBACK='yay -S --noconfirm --needed'
REM='pacman -R --noconfirm'
fi
fi
}
############################################################
# Validate we have superuser access as root (via sudo if requested)
# OUTS: None
#
# Arguments :
# $1 (optional) :
# Set to any value to not attempt root access via sudo
############################################################
function check_superuser() {
local superuser test_euid
if [[ $EUID -eq 0 ]]; then
superuser=true
elif [[ -z ${1-} ]]; then
if check_dep sudo; then
info 'Sudo: Updating cached credentials ...'
if ! sudo -v; then
error "Sudo: Couldn't acquire credentials ..." \
"${fg_red-}"
else
test_euid="$(sudo -H -- "$BASH" -c 'printf "%s" "$EUID"')"
if [[ $test_euid -eq 0 ]]; then
superuser=true
fi
fi
fi
fi
if [[ -z ${superuser-} ]]; then
error 'Unable to acquire superuser credentials.' "${fg_red-}"
return 1
fi
info 'Successfully acquired superuser credentials.'
return 0
}
############################################################
# Run the requested command as root (via sudo if requested)
# OUTS: None
#
# Arguments :
# $1 (optional): Set to zero to not attempt execution via sudo
# $@ (required): Passed through for execution as root user
############################################################
function run_as_root() {
if [[ $# -eq 0 ]]; then
on_exit error 'Missing required argument to run_as_root()!' 2
fi
local try_sudo
if [[ ${1-} =~ ^0$ ]]; then
try_sudo=true
shift
fi
if [[ $EUID -eq 0 ]]; then
"$@"
elif [[ -z ${try_sudo-} ]]; then
sudo -H -- "$@"
else
on_exit error "Unable to run requested command as root: $*" 1
fi
}
############################################################
# Use step(), try(), and next() to perform a series of commands and print
# [ OK ] or [FAILED] at the end. The step as a whole fails if any individual
# command fails.
#
# Example:
# step "Remounting / and /boot as read-write:"
# try mount -o remount,rw /
# try mount -o remount,rw /boot
# next
############################################################
step() {
_log step now "$*"
STEP_OK=0
[[ -w /tmp ]] && echo $STEP_OK > $TEMP_DIR/step
}
try() {
# Check for `-b' argument to run command in the background.
local BG=
[[ $1 == -b ]] && { BG=1; shift; }
[[ $1 == -- ]] && { shift; }
# Run the command.
if [[ -z $BG ]]; then
"$@" > /dev/null 2>&1
else
"$@" > /dev/null 2>&1 &
fi
# Check if command failed and update $STEP_OK if so.
local EXIT_CODE=$?
if [[ $EXIT_CODE -ne 0 ]]; then
STEP_OK=$EXIT_CODE
[[ -w /tmp ]] && echo $STEP_OK > $TEMP_DIR/step
if [[ -n $LOG_STEPS ]]; then
local FILE=$(readlink -m "${BASH_SOURCE[1]}")
local LINE=${BASH_LINENO[0]}
echo "$FILE: line $LINE: Command \`$*' failed with exit code $EXIT_CODE." >> "$LOG_STEPS"
fi
fi
return $EXIT_CODE
}
next() {
[[ -f $TEMP_DIR/step ]] && { STEP_OK=$(< $TEMP_DIR/step); rm -f $TEMP_DIR/step; }
[[ $STEP_OK -eq 0 ]] && _step_success || _step_failure
echo
return $STEP_OK
}
# UTILITY FUNCTIONS
############################################################
# Trim whitespace from a string
#
# Arguments :
# $* (required): String to be trimmed
############################################################
function trim() {
local var="$*"
# remove leading whitespace characters
var="${var#"${var%%[![:space:]]*}"}"
# remove trailing whitespace characters
var="${var%"${var##*[![:space:]]}"}"
# remove space whitespace inside string
var="${var// /}"
echo -n "$var"
}
############################################################
# Convert a string in UpperCase
#
# Arguments :
# $* (required): String to be converted
############################################################
function toUpperCase () {
local var=$*
var=${var^^}
echo -n "$var"
}
############################################################
# Convert a string in LowerCase
#
# Arguments :
# $* (required): String to be converted
############################################################
function toLowerCase () {
local var=$*
var=${var,,}
echo -n "$var"
}
############################################################
# Called on EXIT
#
# Arguments
# $1 : String explaining exit cause
############################################################
function finalize () {
title "Script complete!"
if [[ -f "$OUT_END" ]]; then
cat "$OUT_END"
fi
if [[ $LOG = 1 ]]; then
sed -i 's/\t/ -- /g' "$LOG_FILE"
info "See logs in ${LOG_FILE}"
else
rm --force "$LOG_FILE"
fi
rm --recursive --force --dir "$TEMP_DIR"
_no_more_locking
}
############################################################
# Called first in entry point. Set all necessary things
# No arguments
############################################################
function init () {
# Bash will remember & return the highest exitcode in a chain of pipes.
# This way you can catch the error in case mysqldump fails in `mysqldump | gzip`, for example.
set -o pipefail
# Exit on error. Append '||true' when you run the script if you expect an error.
set -o errexit
# Enable extended globbing (used in option parsing)
shopt -s extglob
# Call finalize when script exit (whatever the return code)
trap 'finalize' EXIT INT TERM
_prepare_locking
exlock_now
set_papckage_manager
}
############################################################
# GLOBAL VARIABLES
############################################################
# Only set when calling get_distro()
OS='' # OS type
VER='' # OS version
# Only set when calling set_papckage_manager()
INST='' # Package manager to use when installing package
INST_FALLBACK='' # Package manager to use if $INST didn't works
REM='' # Package manager to use when removing package
# Options
LOCK=0
QUIET=0
LOG=0
# ./name.sh ==> name.sh
PROGNAME=${0##*/}
# name.sh ==> name, and then trim the string
PROGNAME="$(trim ${PROGNAME%.*})"
SCRIPTPATH="$(realpath $0)"
START_DATE="$(date '+%Y-%m-%d--%H:%M:%S')"
# Useful paths
TEMP_DIR="$(mktemp -d /tmp/XXXXXXX)" # Temporary directory
LOG_FILE=/tmp/log_"$PROGNAME"_["$START_DATE"].txt # Store logs
OUT_END="$TEMP_DIR/out_end.txt" # Store output to display after script execution
LOCKFILE=/tmp/script_lockfile.lock
LOCKFD=99 # File descriptor used when locking the script
# COLORS used in output
NORMAL="\e[0m"
GREEN="\e[32m"
YELLOW="\e[33m"
RED="\e[31m"
BLUE="\e[34m"
CYAN="\e[36m"
BOLD="\e[1m"
priority_options_pattern='@(--debug|-h|--help|--strict|--lock|--logs|-q|--quiet)'
############################################################
# ENTRY POINT
############################################################
init
# First parsing to priority options
for option in $@; do
case $option in
--debug)
set -x
;;
--strict)
set -o nounset
;;
-q|--quiet)
QUIET=1
;;
--logs)
LOG=1
;;
--lock)
LOCK=1
;;
-h|--help)
usage
exit
;;
--) # End of all options.
shift
break
;;
*) # Default case: No more options, so break out of the loop.
continue
esac
done
# Drop lock if not wanted
if [[ LOCK = 0 ]]; then
unlock
fi
# Second parsing
while :; do
case $1 in
--info)
on_exit info "OS : $OS\nVersion : $VER\nPackage manager : $INST"
exit
;;
# Catch unknow options except all priority options (require extended globbing).
$priority_options_pattern)
shift
continue
;;
-?*)
usage "Invalid option $1"
exit
;;
# End of all options.
--)
shift
break
;;
# Default case: No more options, so break out of the loop.
*)
break
esac
shift
done
############################################################
# script goes here
############################################################
exit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment