#!/bin/bash #=============================================================================== # # DIRECTORY: # --- # # FILE: # ./functions.sh # # USAGE: # . functions.sh # OR # source functions.sh # # OPTIONS: # none # # EXIT STATES: # 101 = uncommon binary not installed # 102 = logfile not defined/supplied # 103 = dir for logfile not writable # 104 = logfile already exists but is not writable # 105 = common binary not installed # 106 = your os is not supported (yet) # 107 = illegal browser command supplied # 108 = no case number supplied # 109 = folder not defined/supplied # 110 = folder already exists # 111 = unable to create folder # 112 = ios_backup not supplied # 113 = ios_backup does not exist # 114 = ios_backup is not a directory # 115 = ios_backup is empty # # DESCRIPTION: # Global functions (and configuration library) for scripts. # # REQUIREMENTS: # which, uname, basename, ps, grep, awk, sed, dirname, tee, ... # # BUGS: # --- # # TESTS: # - shellcheck -s bash -e SC2034,SC2016,SC2027,SC2086 ./functions.sh # - shellcheck -s bksh -e SC2034,SC2016,SC2027,SC2086 ./functions.sh # - shellcheck -s dash -e SC2034,SC2016,SC2027,SC2086 ./functions.sh # # AUTHOR: # Patrick Neumann, patrick@neumannsland.de # # COAUTHOR(S): # Odin Heitmann, odin.heitmann@gmail.com # # COMPANY: # (privately) # # VERSION: # 1.0 # # LINK TO THE MOST CURRENT VERSION: # (Sorry, I bet, I'm not allowed to publish it over GitHub!) # # CREATED: # 2018-06-01 # # COPYRIGHT (C): # 2018 - Patrick Neumann & Odin Heitmann # # LICENSE: # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # WARRANTY: # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # NOTES: # --- # # TODO: # - check if HOLE directory (ios_backup) is read-only # - find a more portable solution for [[ in abspath # # HISTORY: # 1.0 - P. N. & O. H. - Initial (for the trainers eyes only) release # #=============================================================================== # Relative paths are more portable but less secure. # Absolute paths are more secure but less portable. readonly BIN_WHICH="/usr/bin/which" # common readonly BIN_AWK="$( ${BIN_WHICH} "awk" )" # common readonly BIN_BASENAME="$( ${BIN_WHICH} "basename" )" # common readonly BIN_DIRNAME="$( ${BIN_WHICH} "dirname" )" # common readonly BIN_GREP="$( ${BIN_WHICH} "grep" )" # common readonly BIN_PS="$( ${BIN_WHICH} "ps" )" # common readonly BIN_TEE="$( ${BIN_WHICH} "tee" )" # common readonly BIN_UNAME="$( ${BIN_WHICH} "uname" )" # common readonly BIN_TR="$( ${BIN_WHICH} "tr" )" # common readonly BIN_LS="$( ${BIN_WHICH} "ls" )" # common readonly BIN_MKDIR="$( ${BIN_WHICH} "mkdir" )" # common readonly BIN_CUT="$( ${BIN_WHICH} "cut" )" # common readonly BIN_RM="$( ${BIN_WHICH} "rm" )" # common readonly BIN_RMDIR="$( ${BIN_WHICH} "rmdir" )" # common readonly BIN_PWD="$( ${BIN_WHICH} "pwd" )" # common #------------------------------------------------------------------------------- # Check existence of uncommon binaries. #------------------------------------------------------------------------------- #[ -z "${BIN_BC}" ] && error_exit "please install bc and retry" 2 # Changeable defaults: DEFAULT_SHELL="bash" ECHO_FUNC="display_and_log" GET_HELP="no" DISABLE_LOGGING="no" DISABLE_STDOUT="no" GET_VERSION_ONLY="no" #------------------------------------------------------------------------------- # Detect the shell in which we are running and set shell depended vars. #------------------------------------------------------------------------------- # SC2016: shellcheck fails if awk/sed is used! readonly PROCESS="$( ${BIN_BASENAME} "$( ${BIN_PS} -axco pid,command \ | ${BIN_GREP} "$$" \ | ${BIN_GREP} -v "grep" \ | ${BIN_AWK} '{ print $2; }' )" )" # Why conditinal command should be prefered over test: # https://google-styleguide.googlecode.com/svn/trunk/shell.xml#Test,_[_and_[[ # and why you don't, if you would support "dash": # http://mywiki.wooledge.org/Bashism if [ "${PROCESS}" = "$( ${BIN_BASENAME} "${0}" )" ] ; then readonly CURRENT_SHELL="${DEFAULT_SHELL}" else readonly CURRENT_SHELL="${PROCESS}" fi # Linux can have alle shells and "/bin/echo" has no limitations. # Darwin (14.5.0) has bash 3.2.57, zsh 5.0.5, ksh 93 and tcsh 6.17.00 # - kshs and tcshs builtin echo does not support "-e" and/or "-n"! # - "/bin/echo" does not support "-e"! # FreeBSD can have all shells, but "/bin/echo" has the same limitations! # Solution: use printf instead! # Worth readable Link: http://hyperpolyglot.org/unix-shells#echo-note if [ "${CURRENT_SHELL}" = "zsh" ] ; then # zsh does not split a string into words separated by spaces by default! setopt shwordsplit # zshs "which" find the builtin without "-p"! readonly BIN_PRINTF="$( which -p printf )" else readonly BIN_PRINTF="$( which printf )" fi if [ "${DEBUG}" = "on" ] ; then ${BIN_PRINTF} "CURRENT_SHELL: %s\\n" "${CURRENT_SHELL}" 1>&2 fi #------------------------------------------------------------------------------- # Detect operating system name and set os depended vars. # (eg. "sed -E" on macOS is the same as "sed -r" on Linux) #------------------------------------------------------------------------------- readonly OS_NAME="$( ${BIN_UNAME} -s )" case "${OS_NAME}" in Darwin) readonly BIN_ID="$( ${BIN_WHICH} "id" )" # common readonly BIN_OPEN="$( ${BIN_WHICH} "open" )" # common readonly SED_EXT_REGEXP="-E" readonly DARWIN_FIND_REGEXP_TYPE="-E " readonly DATE_DISPLAY="-j -f %s " ;; Linux) readonly BIN_GETENT="$( ${BIN_WHICH} "getent" )" # common readonly SED_EXT_REGEXP="--regexp-extended" readonly DATE_DISPLAY="-d @" ;; FreeBSD) readonly BIN_GETENT="$( ${BIN_WHICH} "getent" )" # common readonly SED_EXT_REGEXP="-E" readonly DATE_DISPLAY="-j -f %s " readonly LINUX_FIND_REGEXP_TYPE="-regextype posix-extended " ;; esac if [ "${DEBUG}" = "on" ] ; then # printf has to be assinged before! ${BIN_PRINTF} "OS_NAME: %s\\n" "${OS_NAME}" 1>&2 fi #=== FUNCTION ================================================================== # NAME: check_logfile # DESCRIPTION: Checks if a logfile is set and can be created. # (needed in "log" and "display_and_log"!) # PARAMETERS: none (a global var will be used.) #=============================================================================== # Do not use "function" if you want to support dash: # http://mywiki.wooledge.org/Bashism check_logfile () { if [ -z "${LOG_FILE}" ] ; then # "error_exit" will be defined later! ${BIN_PRINTF} "\\033[01;31;40mERROR: logfile not defined/supplied... EXIT!!!\\033[00m\\n" exit 102 fi if ! [ -w "$( ${BIN_DIRNAME} "${LOG_FILE}" )" ] ; then ${BIN_PRINTF} "\\033[01;31;40mERROR: dir for logfile not writable... EXIT!!!\\033[00m\\n" exit 103 fi if [ -e "${LOG_FILE}" ] ; then if ! [ -w "${LOG_FILE}" ] ; then ${BIN_PRINTF} "\\033[01;31;40mERROR: logfile already exists but is not writable... EXIT!!!\\033[00m\\n" exit 104 else ${BIN_PRINTF} "\\nlogfile does already exist, overwrite? \ (type YES in UPPER letters and hit return!) : " read -r answer if [ "${answer}" != "YES" ] ; then ${BIN_PRINTF} "\\033[01;34;40mHINT: move the old logfile to a save place and try again... EXIT!!!\\033[00m\\n" exit 0 fi # clear logfile ${BIN_PRINTF} "" > "${LOG_FILE}" fi fi } #=== FUNCTION ================================================================== # NAME: display # DESCRIPTION: Wrapper for "echo -n -e". # PARAMETER 1: message (string) #=============================================================================== display () { ${BIN_PRINTF} "${1}" } #=== FUNCTION ================================================================== # NAME: log # DESCRIPTION: Wrapper for "echo -n -e" incl. redirection into logfile. # PARAMETER 1: message (string) #=============================================================================== log () { ${BIN_PRINTF} "${1}" >> "${LOG_FILE}" } #=== FUNCTION ================================================================== # NAME: display_and_log # DESCRIPTION: Wrapper for "echo -n -e" incl. output to stdout AND redirection # into logfile. # PARAMETER 1: message (string) #=============================================================================== display_and_log () { ${BIN_PRINTF} "${1}" | ${BIN_TEE} -a "${LOG_FILE}" } #=== FUNCTION ================================================================== # NAME: quiet # DESCRIPTION: Wrapper for "echo -n -e" incl. redirection into "nirvana". # PARAMETER 1: message (string) #=============================================================================== quiet () { ${BIN_PRINTF} "${1}" > /dev/null } #=== FUNCTION ================================================================== # NAME: error # DESCRIPTION: Display red error messages starting with "ERROR:". # PARAMETER 1: message (string) #=============================================================================== error () { # TODO: stderr too!? "${ECHO_FUNC}" "\\033[01;31;40mERROR: ${1}!!!\\033[00m\\n" } #=== FUNCTION ================================================================== # NAME: error_exit # DESCRIPTION: Display red error messages surrounded by "ERROR:" and "EXIT!!!". # PARAMETER 1: message (string) # PARAMETER 2: exit state (integer) # PARAMETER 3: command (optional) #=============================================================================== error_exit () { "${BIN_PRINTF}" "\\n\\033[01;31;40mERROR: ${1}... EXIT!!!\\033[00m\\n\\n" # Execute command/function before exit if [ -n "${3}" ] ; then ${3} fi # Clean up before exit ${BIN_RM} "${LOG_FILE}" ${BIN_RMDIR} "${FOLDER}" if ! [ "${2}" -eq "${2}" ] 2> /dev/null ; then exit "${2}" else exit 1 fi } #=== FUNCTION ================================================================== # NAME: hint # DESCRIPTION: Display blue hint messages starting with "HINT:". # PARAMETER 1: message (string) #=============================================================================== hint () { "${ECHO_FUNC}" "\\033[01;34;40m${1}\\033[00m\\n" } #=== FUNCTION ================================================================== # NAME: success # DESCRIPTION: Display green success messages starting with "SUCCESS:". # PARAMETER 1: message (string) #=============================================================================== success () { "${ECHO_FUNC}" "\\033[01;32;40mSUCCESS: ${1}!\\033[00m\\n" } #=== FUNCTION ================================================================== # NAME: assign_binary # DESCRIPTION: check if binary exists and store the absolute path in a var. # PARAMETER 1: command (string) #=============================================================================== assign_binary () { # Because in dash, string indexing is not supported we can not use: # "${1:0:1}" = "/" if [ "$( ${BIN_PRINTF} "${1}" | ${BIN_CUT} -c 1 )" = "/" ] ; then # Absolute paths are more secure but less portable. if ! [ -e "${1}" ] ; then error_exit "${1} not installed" 105 fi local var="BIN_$( ${BIN_PRINTF} "$( ${BIN_BASENAME} "${1}" )" \ | ${BIN_TR} "[[:lower:]]" "[[:upper:]]" \ | ${BIN_TR} -d '.' )" eval "readonly $var=\${1}" else # Relative paths are more portable but less secure. local var="BIN_$( ${BIN_PRINTF} "${1}" \ | ${BIN_TR} "[[:lower:]]" "[[:upper:]]" \ | ${BIN_TR} -d '.' )" # SC2027 & SC2086: It's not a bug... it's a feature! eval "readonly $var=\$( ${BIN_WHICH} "${1}" )" if [ -z "$( eval "${BIN_PRINTF} \$$var" )" ] ; then error_exit "${1} not installed" 105 fi fi } #=== FUNCTION ================================================================== # NAME: check_browser # DESCRIPTION: Check for browser (default: firefox) and store in var. # (If no Desktop Environment is installed, try "true" as "a browser".) # PARAMETERS: none (a global var will be used.) #=============================================================================== check_browser () { if [ -z "${BROWSER}" ] ; then case "${OS_NAME}" in Darwin) readonly BROWSER="Firefox" ;; Linux|FreeBSD) BROWSER="firefox" ;; *) error_exit "your os is not supported (yet)" 106 ;; esac fi case "${OS_NAME}" in Darwin) if ! [ -d "/Applications/${BROWSER}.app" ] ; then error_exit "illegal browser command (${BROWSER}) supplied" 107 fi ;; Linux|FreeBSD) readonly BROWSER="$( ${BIN_WHICH} "${BROWSER}" )" if ! [ -x "${BROWSER}" ] ; then error_exit "illegal browser command (${BROWSER}) supplied" 107 fi ;; esac hint "Defined/supplied browser:\\n ${BROWSER}\\n" } #=== FUNCTION ================================================================== # NAME: check_case_number # DESCRIPTION: Check if case number is given and store in var. # PARAMETERS: none (a global var will be used.) #=============================================================================== check_case_number () { if [ -z "${CASE_NUMBER}" ] ; then ${BIN_PRINTF} "\\nNo case number supplied but mandatory - type case numer in and hit return: " read -r case_number if [ -n "${case_number}" ] ; then readonly CASE_NUMBER="${case_number}" ${BIN_PRINTF} "\\n" else error_exit "no case number supplied" 108 usage fi fi hint "Supplied case number:\\n ${CASE_NUMBER}\\n" } #=== FUNCTION ================================================================== # NAME: abspath # DESCRIPTION: generate absolute path from relative path # PARAMETERS 1: string (relative or absolute path to directory or file) # SOURCE: # https://stackoverflow.com/questions/3915040/bash-fish-command-to-print-absolute-path-to-a-file#23002317 #=============================================================================== abspath () { if [ -d "${1}" ]; then # dir ( cd "${1}"; ${BIN_PWD} ) elif [ -f "${1}" ]; then # file if [[ ${1} = /* ]]; then echo "${1}" elif [[ ${1} == */* ]]; then echo "$( cd "${1%/*}"; ${BIN_PWD} )/${1##*/}" else echo "$( ${BIN_PWD} )/${1}" fi fi } #=== FUNCTION ================================================================== # NAME: check_folder # DESCRIPTION: Check if folder does not exist, could be created in the given # location and store in var. # PARAMETERS: none (a global var will be used.) #=============================================================================== check_folder () { if [ -z "${FOLDER}" ] ; then error_exit "folder not defined/supplied" 109 usage fi if [ -d "${FOLDER}" ] ; then # I prefer to check for an existing report folder from a previous call # over "just" check if the folder is empty. error_exit "directory (${FOLDER}) already exists" 110 fi if ! [ -w "$( ${BIN_DIRNAME} "${FOLDER}" )" ] ; then error_exit "unable to create directory (${FOLDER})" 111 fi # Create folder before abspath ${BIN_MKDIR} "${FOLDER}" # because abspath checks for existence! readonly FOLDER="$( abspath "${FOLDER}" )" } #=== FUNCTION ================================================================== # NAME: check_officer # DESCRIPTION: Check if name of officer is given otherwise use # /etc/passwd or OpenDirectory. # PARAMETERS: none (a global var will be used.) #=============================================================================== check_officer () { if [ -z "${OFFICER}" ] ; then case "${OS_NAME}" in Darwin) OFFICER="$( ${BIN_ID} -F )" ;; Linux|FreeBSD) OFFICER="$( ${BIN_GETENT} passwd "${LOGNAME}" \ | cut -d ":" -f 5 | cut -d "," -f 1 )" ;; esac # Sometimes the system don't know the fullname of the user. # The default in Bash on Ubuntu on Windows 10 is "". if [ -z "${OFFICER}" ] || [ "${OFFICER}" = "\"\"" ] ; then readonly OFFICER="unknown" fi fi hint "Guessed/supplied officers name:\\n ${OFFICER}\\n" } #=== FUNCTION ================================================================== # NAME: remove_final_dashes # DESCRIPTION: Check if ios_backup has one or more final dash(es) and # remove them. # PARAMETER 1: string (eg. path to source) #=============================================================================== remove_final_dashes () { # Because in dash, string indexing is not supported we can not use: # "${STRING:(-1)}" = "/" and STRING="${STRING:0:(-1)}" local STRING="${1}" local LENGTH="${#STRING}" while [ "$( ${BIN_PRINTF} "${STRING}" | ${BIN_CUT} -c "${LENGTH}" )" = "/" ] ; do STRING="$( ${BIN_PRINTF} "${STRING}" | ${BIN_CUT} -c 1-$(( LENGTH - 1 )) )" LENGTH="$(( LENGTH - 1 ))" done ${BIN_PRINTF} "%s\\n" "${STRING}" } #=== FUNCTION ================================================================== # NAME: check_source # DESCRIPTION: Check if ios_backup is given and store in var. # Otherwise EXIT! # PARAMETERS: none (a global var will be used.) #=============================================================================== check_source () { if [ "${DEBUG}" = "on" ] ; then ${BIN_PRINTF} "SOURCE: ${1}\\n" 1>&2 fi if [ -z "${1}" ] ; then error_exit "ios_backup not supplied" 112 usage fi if ! [ -e "${1}" ] ; then error_exit "ios_backup (${1}) does not exist" 113 fi if ! [ -d "${1}" ] ; then error_exit "ios_backup (${1}) is not a directory" 114 fi if [ -z "$( ${BIN_LS} "${1}" )" ] ; then error_exit "ios_backup (${1}) is empty" 115 fi # TODO: check if hole directory is read-only IOS_BACKUP="$( remove_final_dashes "${1}" )" readonly IOS_BACKUP="$( abspath "${IOS_BACKUP}" )" hint "Supplied iOS backup folder:\\n ${IOS_BACKUP}\\n" } readonly FUNCTIONS_LOADED="true" if [ "${DEBUG}" = "on" ] ; then ${BIN_PRINTF} "INFO: functions.sh loaded.\\n" 1>&2 fi # Do not use "exit" at the end of a sourced library!