Script-Template
Da ich öfter Bash-Scripte schreibe und immer wieder grundlegende Strukturen benötige, habe ich mir ein Template erstellt, in dem die meisten Funktionen und Strukturen bereits vorhanden sind. Je nach Anforderung lässt es sich leicht anpassen.
Bisher enthaltene Funktionen:
- Automatischer Error-Handler mit ausführlichen Informationen
- Root-Check mit Unterscheidung zwischen sudo und “echtem” lokalen root-Login
- Konfigurierbare Prüfung auf benötigte Kommandos/Tools
- Einheitlicher Message-Handler mit automatischem Logging, Zeitstempeln und farbigen Tags
- Automatischer Start von tmux
- Parameter-Parser mit Kurz- und Langform sowie beliebig vielen unbenannten Parametern am Ende des Aufrufs
- Funktion zur Darstellung zentrierter Texte
- Header mit Logo, Kurzbeschreibung, Version und Slogan des Scripts
- Integrierte Bedienungsanleitung
- Beispielfunktion
test_me
als Einstieg
Dieses Script sowie alle anderen Bash-Snippets, die ich auf meiner Webseite veröffentliche, orientieren sich so gut es geht an Googles Shell Style Guide (unter anderem geprüft mittels shfmt und einer angepassten .editorconfig) und produzieren keine Fehler bei ShellCheck.
Bitte denke daran, dass auch dieses Script nach CC-BY-SA 4.0 lizensiert ist. Du darfst es frei verwenden, anpassen, umschreiben, sogar kommerziell weiterverwenden, aber egal was Du damit tust, die Lizenz muss dieselbe bleiben und Du musst mich in geeigneter Form als Urheber des ursprünglichen Scriptes nennen. Eine komplette Erklärung der Lizenzbedingungen ist verlinkt. In der Regel sollte es ausreichen, die “Written by”- und “License”-Zeilen im Header stehen zu lassen.
Sourcecode
#!/usr/bin/env bash
# Example script v1.0
# Written by Folker "ph0lk3r" Schmidt (info@ichbinein.org) in 2025
# Last updated on 02/2025 by ph0lk3r
# License: CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/legalcode)
#
# This is a short description of the script
#
# Exit codes:
# 0 -> everything is okay
# 1 -> general error
# 2 -> missing privileges
# 3 -> privileged but run as "root" instead of sudo
# 4 -> missing tool
# 5 -> wrong parameter
# 6 -> file not found
# 7 -> directory not found
# 8 -> logpath not writeable
# GLOBALS (constants)
readonly VERSION='1.0'
readonly SLOGAN='Hack the planet!'
readonly DESCRIPTION='A simple example script'
START_DATE_TIME="$(date +'%Y-%m-%d_%H-%M-%S')"
readonly START_DATE_TIME
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
RUNNING_USER="$(logname)"
readonly RUNNING_USER
# REQUIREMENTS
readonly REQUIRED=(tmux lsof nano vim getopt logname)
# GLOBALS (variables)
LOGPATH="/tmp/${SCRIPT_NAME}_${START_DATE_TIME}.log"
LOGPATH_EXISTS=0
PARAMETERS=()
EXAMPLE_FILES=()
EXAMPLE_DIRS=()
COLOUR_GREEN=''
COLOUR_RED=''
COLOUR_CYAN=''
COLOUR_YELLOW=''
COLOUR_GREY=''
COLOUR_NONE=''
COLOUR_BLINK=''
COLOUR_BLINK_RED=''
###########################################################
# Caller for the following error handler function.
# # Globals:
# None
# Arguments:
# None
###########################################################
set_options() {
set -e
set -u
set -o pipefail
set -o errtrace
trap 'catch $? $LINENO $BASH_LINENO ${FUNCNAME[0]} "${PIPESTATUS[*]}"' ERR
}
###########################################################
# General error handler.
# Displays some information about the error and where it
# occurred exactly.
# Globals:
# None
# Arguments:
# $?
# $LINENO
# $BASH_LINENO
# $BASH_COMMAND
# ${FUNCNAME[0]}
# Output:
# Detailed information about the error
# Return values:
# 1: General error
###########################################################
catch() {
local -i error_code
error_code="$1"
local -i error_line
error_line="$2"
local -i calling_line
calling_line="$3"
local error_function
error_function="$4"
local -a pipe_status
IFS=' ' read -r -a pipe_status <<< "$5"
local line_content
line_content="$(sed "${error_line}!d" "$0" | xargs)"
message "c" "Error ${error_code} occurred on line ${error_line}"
message "c" "In function ${error_function}"
message "c" "The function was called at line ${calling_line}"
message "c" "The failing line was:\t\t${line_content}"
if [[ ${#pipe_status[@]} -gt 1 ]]; then
local -a pipe_commands
readarray -d '|' -t pipe_commands <<< "${line_content}"
for command in "${!pipe_commands[@]}"; do
if [[ ${pipe_status[${command}]} -gt 0 ]]; then
local -i field
field=$((command + 1))
message "c" "The failing subcommand was:\t$(echo "${line_content}" \
| cut -d "|" -f ${field} \
| xargs)"
break
fi
done
fi
chown_logfile
exit 1
}
###########################################################
# Check if the script has been called with effective root
# permissions (aka sudo).
# Globals:
# None
# Arguments:
# None
# Return values:
# 2: Script not started with elevated access
# 3: Script started as root
###########################################################
check_root() {
if [[ "${EUID}" -ne 0 ]]; then
message "e" "Please run this script with sudo: \tsudo $0"
exit 2
fi
if [[ "${RUNNING_USER}" == "root" ]]; then
message "e" "Please DON'T run this script as root!"
message "e" "Use your sudo-enabled user account."
exit 3
fi
}
#######################################
# This function simply displays a usage text with all required and optional
# parameters.
# Globals:
# None
# Arguments:
# None
# Outputs:
# Usage information
#######################################
display_usage() {
echo "Usage: $0 [-d|--directory] [-f|--file] [-h|--help] [strings]"
echo ""
echo -e "\t-f|--file\t\t(optional) \
Provide a file to the script, can be used multiple times"
echo -e "\t-d|--directory\t\t(optional) \
Provide a directory to the script, can be used multiple times"
echo -e "\t-h|--help\t\t(optional) Displays this message"
echo -e "\tstring\t\t\t(optional) \
Just a string provided to the script. Must be the last parameter"
echo ""
}
#######################################
# This function checks if the needed tools are being installed and accessible
# and if a tmux session is active. Displays an error message if needed tools
# are missing and starts a new tmux session if there is none active.
# Globals:
# REQUIRED R
# Arguments:
# None
# Return values:
# 4: Required command not found
#######################################
check_requirements() {
local -i fail_counter
fail_counter=0
for tool in "${REQUIRED[@]}"; do
if ! command -v "${tool}" &> /dev/null; then
message "c" "Not found: ${tool}"
fail_counter=$((fail_counter + 1))
fi
done
if [[ ${fail_counter} -gt 0 ]]; then
message "c" "${fail_counter} tools could not be found."
exit 4
fi
}
#######################################
# This function checks if the configured logpath can be written. It tries to create
# the directory and if it fails, it tries to create a local tmp directory to
# write into.
# Globals:
# LOGPATH RW
# START_DATE_TIME R
# Arguments:
# None
# Return values:
# 8: Log path could not be created
#######################################
check_logpath() {
local log_dir
log_dir="$(realpath "$(dirname "${LOGPATH}")")"
if [[ ! -d "${log_dir}" ]]; then
message "w" "Log directory ${log_dir} does not exist. Trying to create..."
if ! mkdir -p "${log_dir}"; then
message "w" "Log directory ${log_dir} could not be created."
message "w" "Trying to create an alternative directory..."
if ! mkdir -p "./tmp"; then
message "c" "Could not create directory $(realpath "./tmp")."
exit 8
else
LOGPATH="$(realpath "./")/${SCRIPT_NAME}_${START_DATE_TIME}.log"
fi
fi
fi
LOGPATH_EXISTS=1
}
#######################################
# This function checks the script runs inside a tmux session and restarts it
# inside tmux if necessary.
# Globals:
# START_DATE_TIME R
# RUNNING_USER R
# Arguments:
# $@
#######################################
check_tmux() {
if [[ ! "$TERM" =~ screen.*|tmux.* ]]; then
echo -e "tmux environment not found. Starting a new instance in tmux."
sleep 1
tmux -2 new-session -d -s "${RUNNING_USER}_${START_DATE_TIME}"
sleep 1
tmux -2 send "$0 $*" ENTER
tmux -2 attach -t "${RUNNING_USER}_${START_DATE_TIME}"
exit 0
fi
}
#######################################
# This function handles all command line parameters that the script received.
# It receives a list of all given parameters, sets variables according to their
# presence and displays an error message when parameterss are wrong or missing.
# Globals:
# None
# Arguments:
# $@
# Return values:
# 5: Wrong argument
# 6: File not found
# 7: Directory not found
#######################################
get_params() {
if ! ARGS="$(getopt -o "f:d:h" --long "file:directory:help" -- "$@")"; then
echo ""
display_usage
exit 5
fi
eval set -- "${ARGS}"
while [[ -n ${1+x} ]]; do
case "$1" in
-f | --file)
if [ -z "$2" ] || [ ! -f "$2" ]; then
message "e" "-f/--file option requires a valid file path."
exit 6
fi
EXAMPLE_FILES+=("$2")
shift 2
;;
-d | --directory)
if [ -z "$2" ] || [ ! -d "$2" ]; then
message "e" "-d/--directory option requires a valid directory path."
exit 7
fi
EXAMPLE_DIRS+=("$2")
shift 2
;;
-h | --help)
display_usage
exit 0
;;
--)
shift
;;
*)
if [[ -z ${1+x} ]]; then
break
else
PARAMETERS+=("$1")
fi
shift
;;
esac
done
}
#######################################
# If the terminal supports colored output, the colors are set by this function.
# Globals:
# COLOUR_GREEN W
# COLOUR_RED W
# COLOUR_CYAN W
# COLOUR_YELLOW W
# COLOUR_GREY W
# COLOUR_NONE W
# COLOUR_BLINK W
# COLOUR_BLINK_RED W
# Arguments:
# None
#######################################
set_colours() {
if [[ "$(tput colors)" -ne -1 ]]; then
COLOUR_GREEN='\033[0;32m'
COLOUR_RED='\033[0;31m'
COLOUR_CYAN='\033[0;36m'
COLOUR_YELLOW='\033[0;93m'
COLOUR_GREY='\033[0;90m'
COLOUR_NONE='\033[0;0m'
COLOUR_BLINK='\033[0;5m'
COLOUR_BLINK_RED='\033[31;5m'
fi
}
#######################################
# This function centers a text. It has a static width of 50 columns configured
# because it is only used by the display_header function
# Globals:
# None
# Arguments:
# Text to be centered
# Outputs:
# Centered text
#######################################
display_center() {
local columns
local centerline
columns="52"
centerline="$1"
printf "%*s\n" $(((${#centerline} + columns) / 2)) "${centerline}"
}
#######################################
# Displays a description, version and a slogan.
# Globals:
# None
# Arguments:
# None
# Outputs:
# A header logo and script information
#######################################
display_header() {
echo ''
echo '░▒▓█▓▒░ ░▒▓██████▓▒░ ░▒▓██████▓▒░ ░▒▓██████▓▒░ '
echo '░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ '
echo '░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ '
echo '░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒▒▓███▓▒░▒▓█▓▒░░▒▓█▓▒░ '
echo '░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ '
echo '░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ '
echo '░▒▓████████▓▒░▒▓██████▓▒░ ░▒▓██████▓▒░ ░▒▓██████▓▒░ '
echo
display_center "${DESCRIPTION}"
display_center "v${VERSION} \"${SLOGAN}\""
echo ''
}
#######################################
# This function is the main message parser used for text output.
# It handles the prepended timecode, colours and the following message types:
# - okay: Best used after a "waiting"" message for marking successful execution
# - wait: Displays a waiting status which will be replaced by the next message
# - warning: Displays a warning tag for non-functioning warnings
# - error: Displays an error tag for failed commands
# - critical: Displays a message signalling immediate danger
# - debug: Displays debug messages when the debug parameter is set
# - no message type: Displays normal output as "INFO"
# Colours are handled automatically and should not be set within the messages
# manually.
# This function is also responsible to write all messages to the log file
# "${LOGPATH}"
# Globals:
# LOGPATH R
# Arguments:
# message_type: Type as of the list above; optional
# message_text: First parameter, containing the text to be displayed
# Outputs:
# Formatted text
#######################################
message() {
local message_type
local message_text
local message_tag
local message_suffix
local colour_prefix
local colour_suffix
local timecode_prefix
local -i is_error
message_text=""
message_type=""
message_tag=""
colour_prefix=""
colour_suffix=""
message_suffix="\n"
timecode_prefix="$(date +'%F %T')\t"
is_error=0
set_colours
if [[ "$#" -ne 1 ]]; then
message_text="$2"
message_type="$1"
case "${message_type}" in
e | err | error)
message_tag="FAIL"
colour_prefix="${COLOUR_RED}"
colour_suffix="${COLOUR_NONE}"
is_error=1
;;
w | warn | warning)
message_tag="WARN"
colour_prefix="${COLOUR_YELLOW}"
colour_suffix="${COLOUR_NONE}"
is_error=1
;;
c | crit | critical)
message_tag="CRIT"
colour_prefix="${COLOUR_BLINK_RED}"
colour_suffix="${COLOUR_NONE}"
is_error=1
;;
wait)
message_tag="WAIT"
colour_prefix="${COLOUR_BLINK}"
colour_suffix="${COLOUR_NONE}"
message_suffix="\033[0K\r"
;;
o | ok | okay)
message_tag="OKAY"
colour_prefix="${COLOUR_GREEN}"
colour_suffix="${COLOUR_NONE}"
;;
d | debg | debug)
message_tag="DEBG"
colour_prefix="${COLOUR_GREY}"
colour_suffix="${COLOUR_NONE}"
;;
*)
message_tag="INFO"
colour_prefix="${COLOUR_NONE}"
colour_suffix="${COLOUR_NONE}"
;;
esac
else
message_tag="XXXX"
colour_prefix="${COLOUR_CYAN}"
colour_suffix="${COLOUR_NONE}"
message_text="$1"
fi
if [[ ${is_error} -eq 1 ]]; then
echo -ne "\033[2K${timecode_prefix}[${colour_prefix}${message_tag}\
${colour_suffix}]\t${message_text}${message_suffix}" >&2
else
echo -ne "\033[2K${timecode_prefix}[${colour_prefix}${message_tag}\
${colour_suffix}]\t${message_text}${message_suffix}"
fi
if [[ ${LOGPATH_EXISTS} -eq 1 ]]; then
echo -e "${timecode_prefix}[${message_tag}]\t${message_text}" >> "${LOGPATH}"
fi
}
#######################################
# This function just displays some messages
# Globals:
# LOGPATH R
# RUNNING_USER R
# Arguments:
# None
#######################################
chown_logfile() {
chmod 660 "${LOGPATH}"
chown "${RUNNING_USER}" "${LOGPATH}"
}
#######################################
# This function just displays some messages
# Globals:
# EXAMPLE_FILES R
# EXAMPLE_DIRS R
# PARAMETERS R
# RUNNING_USER R
# Arguments:
# None
#######################################
test_me() {
display_header
message "Try this script with $0 -d /tmp -d /home \
-f /home/${RUNNING_USER}/.bashrc -f /etc/passwd this is a test"
if [[ "${#EXAMPLE_FILES[@]}" -gt 0 ]]; then
message "o" "Example files (${#EXAMPLE_FILES[@]}): ${EXAMPLE_FILES[*]}"
fi
if [[ "${#EXAMPLE_DIRS[@]}" -gt 0 ]]; then
message "o" "Example dirs (${#EXAMPLE_DIRS[@]}): ${EXAMPLE_DIRS[*]}"
fi
if [[ "${#PARAMETERS[@]}" -gt 0 ]]; then
message "w" "Additional parameters (${#PARAMETERS[@]}): ${PARAMETERS[*]}"
fi
message "i" "Running as ${RUNNING_USER}"
message "wait" "Is that really all folks?"
sleep 2
message "d" "That's all folks!"
message "Log file written to ${LOGPATH}"
message "e" "The next line will provoke an error in two seconds!"
sleep 2 | true | false | /bin/true | /bin/false
}
#######################################
# Finally, the main function. It orchestrates all the other functions and makes
# sure that each of them gets all the parameters it needs and that they are
# called in the correct order.
# Globals:
# None
# Arguments:
# $@
#######################################
main() {
set_options
get_params "$@"
check_logpath
check_requirements
check_root
check_tmux "$@"
test_me "$@"
chown_logfile
}
main "$@"