# # Author: Georg Voell - georg.voell@standby.cloud # Version: @(#)lib.bash 3.3.1 16.03.2026 (c)2026 Standby.cloud # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ # # This script can be used free of charge. Use it as is or customize as needed. It is not guaranteed to be # error free and you will not be reimbursed for any damage it may cause. # # To include with "source lib.bash" # Requirements: bash >= 4.3 # # Update history: # # V 3.0.0 18.04.2020 New version # V 3.0.1 02.07.2020 Show version (only version - no toolname) / Avoid 'abort by user message' in 'confirm' and 'errormsg' # V 3.0.2 19.01.2021 New function ClearSTDIN # V 3.1.0 05.06.2023 New copyright # V 3.1.1 07.09.2023 Use gnu sed and tail if available # V 3.1.2 23.07.2024 New resource types # V 3.2.0 11.08.2024 New minor version: New function ConvertKeys # V 3.2.1 30.11.2024 ConvertKeys: Tab is the delimiter now / New functions: NumKeys and GetKey # V 3.2.2 06.11.2025 Function InstanceInfo # V 3.3.0 27.11.2025 Revised with support of ChatGPT # V 3.3.1 16.03.2026 Optimized: removed debug echo in ToLower; replaced backticks with $(); # replaced [ ] with [[ ]]; removed subshells in BuildPath, ShrinkString, # ConvertOCID, CheckKey, ConvertKeys, AdjustResourceType, AdjustRegion, # InstanceInfo, DisplayHelp; replaced let/[ -a ] with (( ))/&& # # Prevent accidental execution if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then echo "This file is a library and must be sourced, not executed." >&2 exit 1 fi # Test if function is defined # FunctionExists: return 'true' or 'false' or 'unknown' function FunctionExists { local name="$1" if [[ -z "$name" ]]; then printf '%s\n' "unknown" return 2 fi if [[ "$(type -t -- "$name" 2>/dev/null)" == "function" ]]; then printf '%s\n' "true" return 0 else printf '%s\n' "false" return 1 fi } # trap ctrl-c and call interrupt() trap Interrupt INT # Check if functions are already declared if [[ "$(FunctionExists Cleanup)" != "true" ]]; then # Do cleanup function Cleanup { if [[ -f "$scratchfile" ]]; then rm -f "$scratchfile" fi if [[ "$(FunctionExists ExtraCleanup)" == "true" ]]; then ExtraCleanup fi } # Clear /dev/stdin - Clear Pipe function ClearSTDIN { # Check if we have something in input stream if [[ ! -t 0 ]]; then cat /dev/stdin > /dev/null 2>&1 fi } # Do cleanup, display error message and exit function Interrupt { exitcode=99 if [[ -n "$itrfile" ]]; then if [[ ! -f "$itrfile" ]]; then # Print newline and bell printf "${bell}\n" >/dev/tty # Display abort messages errormsg $exitcode "Script aborted by user interrupt." >/dev/tty 2>&1 # Create user interrupt file touch "$itrfile" if [[ -n "$SUDO_USER" ]]; then chown "${REALUSER}:$REALGROUP" "$itrfile" fi # Change filerights - group-readable so cascading scripts can detect interrupt chmod 664 "$itrfile" fi fi # Delete temp files, clear pipe, leave the script and return errorcode Cleanup ClearSTDIN exit $exitcode } # Dual-mode lowercasing: # 1) Use arguments # 2) Otherwise -> If STDIN is not a terminal -> read from pipe or redirection function ToLower { local s="$*" if [[ -z "$s" ]]; then # If input comes from a pipe or file, use STDIN mode if [[ ! -t 0 ]]; then tr '[:upper:]' '[:lower:]' fi else # Use bash built-in expansion when possible (faster, no subshell) if [[ "${BASH_VERSINFO[0]:-0}" -ge 4 ]]; then printf '%s' "${s,,}" else printf '%s' "$s" | tr '[:upper:]' '[:lower:]' fi fi } # Dual-mode uppercasing: # 1) Use arguments # 2) Otherwise -> If STDIN is not a terminal -> read from pipe or redirection function ToUpper { local s="$*" if [[ -z "$s" ]]; then # If input comes from a pipe or file, use STDIN mode if [[ ! -t 0 ]]; then tr '[:lower:]' '[:upper:]' fi else # Use bash built-in expansion when possible (faster, no subshell) if [[ "${BASH_VERSINFO[0]:-0}" -ge 4 ]]; then printf '%s' "${s^^}" else printf '%s' "$s" | tr '[:lower:]' '[:upper:]' fi fi } # StripComment: remove trailing comments starting with '#' function StripComment { local file="$*" if [[ -n "$file" ]]; then if [[ -s "$file" ]]; then sed -E 's/([[:space:]]|^)\#.*$//' "$file" fi else # If input comes from a pipe or file, use STDIN mode if [[ ! -t 0 ]]; then sed -E 's/([[:space:]]|^)\#.*$//' fi fi } # Delimit strings to given size function ShrinkString { local str="${1}" local slen="${2}" local size=0 if [[ -n "$str" ]]; then # Use bash built-in ${#} instead of wc -m (no subshell) size=${#str} if [[ -z "$slen" ]]; then slen=$size fi if (( size > slen )); then (( slen-- )) printf '%s*\n' "${str:0:$slen}" else printf '%s\n' "$str" fi fi } # Used with DEBUG function ShowVariable { local varname="${1}" local content="${2}" local stat=0 if [[ -n "$LOGFILE" ]]; then # Check if $LOGFILE exists - otherwise create it if [[ ! -f "$LOGFILE" ]]; then touch "$LOGFILE" > /dev/null 2>&1 stat=$? fi if [[ -n "$varname" && $stat -eq 0 ]]; then printf "DEBUG (%s): %s: '%s'\n" "$progstr" "$varname" "$content" >> "$LOGFILE" fi fi if [[ -n "$varname" ]]; then printf "DEBUG (%s): %s: '%s'\n" "$progstr" "$varname" "$content" > /dev/tty fi } ### Define PATH function BuildPath { local orgpath="${1}" local locpath="/usr/local/sbin /usr/local/bin" local stdpath="/usr/sbin /usr/bin /sbin /bin" local newpath="" local addpath="" local usrpath="" local curpath="" local folder="" if [[ "$HOME" == "/" ]]; then usrpath="/.local/bin" else if [[ "$REALHOME" != "$HOME" ]]; then usrpath="${HOME}/bin ${HOME}/.local/bin ${REALHOME}/bin ${REALHOME}/.local/bin" else usrpath="${REALHOME}/bin ${REALHOME}/.local/bin" fi fi # Convert colon-separated PATH to space-separated list (no subshell needed) curpath="${orgpath//:/ }" case "$OS" in Darwin) addpath="/opt/local/sbin /opt/local/bin /opt/X11/bin" ;; SunOS) addpath="/usr/gnu/bin /usr/sfw/bin /opt/sfw/bin /opt/csw/sbin /opt/csw/bin" ;; esac for folder in $usrpath $locpath $addpath $stdpath $curpath; do # Use bash pattern match instead of grep subshell (much faster in loops) if [[ -d "$folder" && ":${newpath}:" != *":${folder}:"* ]]; then if [[ -z "$newpath" ]]; then newpath="$folder" else newpath="${newpath}:${folder}" fi fi done # Return result printf '%s\n' "$newpath" } # Get the number of values in a string separated by tab function NumKeys { local keystring="${1}" local IFS=$'\t' local fields=() read -r -a fields <<< "$keystring" printf '%s\n' "${#fields[@]}" } # Get the nth value of a string separated by tab function GetKey { local keystring="$1" local nth="$2" local IFS=$'\t' local fields=() # validate nth: must be a positive integer [[ "$nth" =~ ^[1-9][0-9]*$ ]] || { printf '\n'; return; } # split into array read -r -a fields <<< "$keystring" # nth is 1-based; Bash arrays are 0-based (( nth-- )) # return empty if index out of range [[ $nth -lt ${#fields[@]} ]] && printf '%s\n' "${fields[$nth]}" || printf '\n' } # Search for key and return value of a string separated by tab function GrepKey { local keystring="$1" local key="$2" local k local v [[ -z "$keystring" || -z "$key" ]] && { printf '\n'; return; } # Process each line in keystring (no subshell) while IFS=$'\t' read -r k v; do if [[ "$k" == "$key" ]]; then printf '%s\n' "$v" return fi done <<< "$keystring" printf '\n' } # Check if key is part of keystring function CheckKey { local keystring="${1}" local key="${2}" local search="${3}" local found="" if [[ -n "$keystring" && -n "$key" ]]; then # tr+grep pipeline is unavoidable here for pattern matching across tab-fields case "$search" in parent) found=$(printf '%s' "$keystring" | tr '\t' '\n' | grep -a "^${key}/") ;; child) found=$(printf '%s' "$keystring" | tr '\t' '\n' | grep -a "/${key}$") ;; *) found=$(printf '%s' "$keystring" | tr '\t' '\n' | grep -ia "^${key}$") ;; esac fi printf '%s\n' "$found" } # Convert a tab (preferred), colon or comma separated string to tab separated string function ConvertKeys { local keystring="${1}" local result="" if [[ -n "$keystring" ]]; then if [[ "$keystring" == *$'\t'* ]]; then # Fields are already separated by tab (preferred) - only squeeze tabs result=$(printf '%s' "$keystring" | tr -s '\t') elif [[ "$keystring" == *","* ]]; then # We assume that Fields are separated by comma result=$(printf '%s' "$keystring" | tr -d '\t' | tr -s ',' '\t') elif [[ "$keystring" == *":"* ]]; then # Fields are separated by colon result=$(printf '%s' "$keystring" | tr -d '\t' | tr -s ':' '\t') else # No separator - return keystring as-is result="$keystring" fi fi printf '%s\n' "$result" } # Sometimes the resource type name from OCID doesn't match the real resource type function AdjustResourceType { local rtype="${1}" if [[ -n "$rtype" ]]; then # Use bash built-in lowercasing (no subshell) rtype="${rtype,,}" case "$rtype" in blockvolumereplica) rtype="volumereplica" ;; certificatesassociation) rtype="certificateauthorityassociation" ;; cluster) rtype="clusterscluster" ;; computecontainer) rtype="container" ;; computecontainerinstance) rtype="containerinstance" ;; contentexperiencecloudservice) rtype="oceinstance" ;; datasafetargetalertpolicyassoc) rtype="datasafetargetalertpolicyassociation" ;; dns-zone) rtype="customerdnszone" ;; domainapp) rtype="app" ;; dynamicgroup) rtype="dynamicresourcegroup" ;; floatingip) rtype="publicip" ;; fnapp) rtype="functionsapplication" ;; fnfunc) rtype="functionsfunction" ;; recoveryserviceprotecteddatabase) rtype="protecteddatabase" ;; saml2idp) rtype="identityprovider" ;; esac printf '%s\n' "$rtype" fi } # Sometimes the region name from OCID doesn't match the real region name function AdjustRegion { local region="${1}" if [[ -n "$region" ]]; then # Use bash built-in lowercasing (no subshell) region="${region,,}" case "$region" in phx) region="us-phoenix-1" ;; iad | us-ashburn) region="us-ashburn-1" ;; esac printf '%s\n' "$region" fi } # Convert an OCID into its parts and return it as tsv # https://docs.oracle.com/en-us/iaas/Content/General/Concepts/identifiers.htm # ocid1...[REGION][.FUTURE USE]. function ConvertOCID { local ocid="${1}" local vers="" local rtype="" local realm="" local region="" local uid="" local use="" local dc="" local result="" local stat=0 if [[ -n "$ocid" ]]; then # Check if ocid starts with 'ocid' and contains dots (no subshell) if [[ "$ocid" == ocid* && "$ocid" == *.* ]]; then # Use read + IFS to split by '.' (replaces 6 cut subshells) IFS='.' read -r vers rtype realm region use uid <<< "$ocid" if [[ -z "$uid" ]]; then uid="$use" use="" fi # Convert additional parameter if [[ "$vers" == "ocid1" && -n "$rtype" && -n "$uid" ]]; then # Convert resource type rtype=$(AdjustResourceType "$rtype") # Convert region region=$(AdjustRegion "$region") # Convert uid to dc if [[ -n "$uid" ]]; then result=$(filecheck -x base32) if [[ -n "$result" ]]; then dc=$(printf '%s' "$uid" | head -c 8 | ToUpper | base32 -d | tr -d '\0') else dc=$(curl -skL --connect-timeout 5 "https://standby.cloud/cgi-bin/get-dc.pl?param=$uid" 2>/dev/null) stat=$? if [[ $stat -ne 0 ]]; then dc="" fi fi fi # Print result printf "version\tresourceType\trealm\tregion\tuse\tuid\tdc\n" printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "$vers" "$rtype" "$realm" "$region" "$use" "$uid" "$dc" fi fi fi } # Get infos about current instance from instance metadata service (IMDS) function InstanceInfo { local infotype="${1}" local imdsurl="http://169.254.169.254/opc/v2" local auth="Authorization: Bearer Oracle" local keyword="" local stat=1 if [[ -n "$infotype" ]]; then # Normalize slashes without a subshell infotype="/${infotype}/" infotype="${infotype//\/\//\/}" # Extract keyword between first pair of slashes keyword="${infotype#/}" keyword="${keyword%%/*}" case "$keyword" in instance | vnics | identity) curl -skL -H "$auth" "${imdsurl}${infotype}" | grep -v "^<" stat=$? ;; esac fi return $stat } # Display help or error message function DisplayHelp { local commentvers="" local namestr="" local cversion="" local cright="" local cowner="" local versstr="" if [[ "$showversion" == true || "$showhelp" == true || -n "$errstr" ]]; then # Get the version of the script (single pipeline, fewer subshells) commentvers=$(grep '^# Version: @(' "$script" | head -n 1 | cut -d')' -f2-) namestr="${commentvers%% *}" # first word cversion=$(printf '%s' "$commentvers" | cut -d' ' -f2) cright=$(printf '%s' "$commentvers" | cut -d' ' -f4) cowner=$(printf '%s' "$commentvers" | cut -d' ' -f5-) if [[ "$showversion" == true ]]; then printf "%s\n" "$cversion" else # Build version string versstr="${namestr} ${cversion}, ${cright} ${cowner}" printf "\n%s\n" "$versstr" grep '^#@' "$script" | sed 's|^#@||' printf "\n" # Display error message and exit with return code 1 if [[ "$showhelp" == false && -n "$errstr" ]]; then exitcode=1 errormsg $exitcode "$errstr" fi fi # Delete temp files, clear pipe, leave the script and return errorcode Cleanup ClearSTDIN exit $exitcode fi } fi # Get the REALUSER if [[ -z "$REALUSER" ]]; then export USER=$(whoami) # Current user export OS=$(uname -s) # Infos about the host os (e.g. Darwin, SunOS, Linux) # Try to get HOME if it is not set if [[ -z "$HOME" || "$HOME" == "/" ]]; then export HOME=$(eval echo "~${USER}") if [[ -z "$HOME" ]]; then export HOME="/" fi fi # Check if script runs with 'sudo' if [[ -n "$SUDO_USER" ]]; then export REALUSER="$SUDO_USER" export REALGROUP=$(id -gn "$SUDO_USER") export REALHOME=$(eval echo "~${SUDO_USER}") export HOME=$(eval echo "~${USER}") else export REALUSER="$USER" export REALGROUP=$(id -gn "$USER") export REALHOME="$HOME" fi # Show debug infos if asked for if [[ "$DEBUG_LIB" == true ]]; then ShowVariable "USER" "$USER" ShowVariable "HOME" "$HOME" ShowVariable "OS" "$OS" ShowVariable "SUDO_USER" "$SUDO_USER" ShowVariable "REALUSER" "$REALUSER" ShowVariable "REALHOME" "$REALHOME" ShowVariable "USER_HOME" "$HOME" fi fi # Define global default variables if [[ -z "$script" ]]; then if [[ -n "$scriptname" ]]; then script="$scriptname" else # Strip leading dashes (bash built-in, no sed subshell) script="${0#-}" script="${script##-}" # Robust strip of all leading dashes while [[ "$script" == -* ]]; do script="${script#-}"; done fi readonly progstr=$(basename "$script") # Basename of the script readonly progdir=$(dirname "$script") # Dirname of the script readonly pid="$$" # Process id readonly tmpdir="/tmp" # Temporary directory readonly timestamp=$(date '+%y%m%d%H%M%S') # Date/time stamp readonly scratchfile="${tmpdir}/${progstr}.${timestamp}.${pid}.tmp" # Temporary file readonly itrfile="${tmpdir}/${REALUSER}_INTERRUPT.tmp" # Created on Ctrl+C exitcode=0 # Default exit code showhelp=false # true to show help showversion=false # true to show version errstr="" # Default error message result="" # Result string fi # Check if user interrupt file exists (was created by the last user interrupt) and delete it if [[ -n "$itrfile" && -f "$itrfile" ]]; then if [[ "$progstr" != "errormsg" && "$progstr" != "filecheck" && \ "$progstr" != "oci-api" && "$progstr" != "convert-json" ]]; then rm -f "$itrfile" fi fi # Show debug infos if asked for if [[ "$DEBUG_LIB" == true ]]; then ShowVariable "script" "$script" ShowVariable "scratchfile" "$scratchfile" fi # Define some colors and bell if [[ -z "$normal" ]]; then readonly normal=$'\033[0m' readonly bell=$'\007' readonly red=$'\033[0;31m' readonly green=$'\033[0;32m' readonly yellow=$'\033[0;33m' readonly blue=$'\033[0;34m' readonly magenta=$'\033[0;35m' readonly cyan=$'\033[0;36m' readonly gray=$'\033[0;90m' readonly white=$'\033[0;97m' readonly black=$'\033[0;30m' readonly bold=$'\033[1m' readonly highred=$'\033[0;91m' # Highlighted red readonly pink=$'\033[0;95m' # Highlighted magenta fi # Set PATH to something useful if [[ -z "$WORKPATH" ]]; then export CURRPATH="$PATH" export WORKPATH=$(BuildPath "${PATH}:${progdir}") export PATH="$WORKPATH" # Show debug infos if asked for if [[ "$DEBUG_LIB" == true ]]; then ShowVariable "CURRPATH" "$CURRPATH" ShowVariable "WORKPATH" "$WORKPATH" fi fi # Define helpful aliases result=$(alias taillastline 2>/dev/null) if [[ -z "$result" ]]; then shopt -s expand_aliases # Check if we have gnu tools installed (gsed, gtail) for tool in sed tail; do result=$(which "g${tool}" 2>/dev/null) if [[ -n "$result" ]]; then alias $tool="$result" fi done if [[ "$OS" == "SunOS" ]]; then # On Solaris, tail behaves differently if [[ -x "/usr/gnu/bin/tail" ]]; then alias tailfromline2="/usr/gnu/bin/tail -n +2" alias taillastline="/usr/gnu/bin/tail -n 1" else alias tailfromline2="/usr/bin/tail +2" alias taillastline="/usr/bin/tail -1" fi else alias tailfromline2="tail -n +2" alias taillastline="tail -n 1" fi fi