diff --git a/call_host.sh b/call_host.sh index 3535b12..c98644e 100755 --- a/call_host.sh +++ b/call_host.sh @@ -1,4 +1,5 @@ #!/bin/bash +# Print each command before executing it (for debugging) # shellcheck disable=SC2155,SC2223 # check for configuration @@ -8,15 +9,70 @@ if [ -f "$CALL_HOST_CONFIG" ]; then source "$CALL_HOST_CONFIG" fi +# zsh / bash compatibility helpers +is_zsh(){ + # detect the current shell process name (portable ps usage) + if command -v ps >/dev/null 2>&1; then + # get last path component if ps returns full path + p="$(ps -p $$ -o comm= 2>/dev/null | awk -F/ '{print $NF}')" + case "$p" in + zsh) return 0 ;; + esac + fi + + # fallback: check common names for $0 or $ZSH_NAME (login shells may have a leading dash) + case "$(basename -- "${ZSH_NAME:-$0}" 2>/dev/null)" in + zsh|-zsh) return 0 ;; + esac + + return 1 +} + +if is_zsh; then + # export a function to the environment for child shells (zsh) + export_func(){ + typeset -fx "$1" 2>/dev/null || true + } + # declare an associative array (zsh) - create a named array using eval so dynamic name works + declare_assoc(){ + eval "typeset -A $1" + } + # get current function name in zsh (be tolerant if indices differ) + current_funcname(){ + echo "${funcstack[1]:-${funcstack[0]:-}}" + } +else + # bash + export_func(){ + [ -n "$1" ] || return + eval "export -f $1" 2>/dev/null || true + } + declare_assoc(){ + # create named associative array in bash + eval "declare -A $1" + } + current_funcname(){ + # return the caller function name if available (FUNCNAME[1]), otherwise fall back to FUNCNAME[0] + if [ -n "${FUNCNAME[1]:-}" ]; then + echo "${FUNCNAME[1]}" + else + echo "${FUNCNAME[0]:-}" + fi + } +fi + # validation call_host_valid(){ VAR_TO_VALIDATE="$1" - # shellcheck disable=SC2076 - if [[ ! " enable disable " =~ " ${!VAR_TO_VALIDATE} " ]]; then - echo "Warning: unsupported value ${!VAR_TO_VALIDATE} for $VAR_TO_VALIDATE; disabling" + # retrieve the value of the named variable in a way that works in bash and zsh + VARVAL="$(eval "printf '%s' \"\${$VAR_TO_VALIDATE}\"")" + # check allowed values in a POSIX-compatible way + if [ "$VARVAL" != "enable" ] && [ "$VARVAL" != "disable" ]; then + echo "Warning: unsupported value $VARVAL for $VAR_TO_VALIDATE; disabling" eval "export $VAR_TO_VALIDATE=disable" fi } +export_func call_host_valid # default values : ${CALL_HOST_STATUS:=enable} @@ -46,18 +102,41 @@ if [ ! -d "$CALL_HOST_DIR" ]; then echo "Warning: could not create specified dir CALL_HOST_DIR $CALL_HOST_DIR. disabling" export CALL_HOST_STATUS=disable fi -# ensure the pipe dir is bound -export APPTAINER_BIND=${APPTAINER_BIND}${APPTAINER_BIND:+,}${CALL_HOST_DIR} + +# helper: add value to a PATH-like variable only if not already present +add_path_unique(){ + # args: varname value [sep] + varname="$1"; val="$2"; sep="${3:-:}" + # retrieve current value portably + cur="$(eval "printf '%s' \"\${${varname}:-}\"")" + # if empty, set and export + if [ -z "$cur" ]; then + eval "export $varname=\"\$val\"" + return + fi + # check for whole-element match using separators to avoid substrings + case "${sep}${cur}${sep}" in + *"${sep}${val}${sep}"*) + # already present + return + ;; + esac + # append with separator + eval "export $varname=\"${cur}${sep}${val}\"" +} + +# ensure the pipe dir is bound (use comma separator for APPTAINER_BIND) +add_path_unique APPTAINER_BIND "$CALL_HOST_DIR" "," # enable/disable toggles call_host_enable(){ export CALL_HOST_STATUS=enable } -export -f call_host_enable +export_func call_host_enable call_host_disable(){ export CALL_HOST_STATUS=disable } -export -f call_host_disable +export_func call_host_disable # single toggle for debug printouts call_host_debug(){ if [ "$CALL_HOST_DEBUG" = "enable" ]; then @@ -66,18 +145,19 @@ call_host_debug(){ export CALL_HOST_DEBUG=enable fi } -export -f call_host_debug +export_func call_host_debug # helper for debug printouts call_host_debug_print(){ if [ "$CALL_HOST_DEBUG" = "enable" ]; then echo "$@" fi } -export -f call_host_debug_print +export_func call_host_debug_print call_host_plugin_01(){ # provide htcondor-specific info in container - declare -A CONDOR_OS + # portable associative-array declaration + declare_assoc CONDOR_OS CONDOR_OS[7]="SL7" CONDOR_OS[8]="EL8" CONDOR_OS[9]="EL9" @@ -93,7 +173,7 @@ call_host_plugin_01(){ fi fi } -export -f call_host_plugin_01 +export_func call_host_plugin_01 # concept based on https://stackoverflow.com/questions/32163955/how-to-run-shell-script-on-host-from-docker-container @@ -113,7 +193,7 @@ listenhost(){ echo "$tmpexit" > "$3" done } -export -f listenhost +export_func listenhost # creates randomly named pipe and prints the name makepipe(){ @@ -122,7 +202,7 @@ makepipe(){ mkfifo "$PIPETMP" echo "$PIPETMP" } -export -f makepipe +export_func makepipe # to be run on host before launching each apptainer session startpipe(){ @@ -132,16 +212,19 @@ startpipe(){ # export pipes to apptainer echo "export APPTAINERENV_HOSTPIPE=$HOSTPIPE; export APPTAINERENV_CONTPIPE=$CONTPIPE; export APPTAINERENV_EXITPIPE=$EXITPIPE" } -export -f startpipe +export_func startpipe # sends function to host, then listens for output, and provides exit code from function call_host(){ # disable ctrl+c to prevent "Interrupted system call" trap "" SIGINT - if [ "${FUNCNAME[0]}" = "call_host" ]; then + + # determine caller function name in a portable way + CURFN="$(current_funcname)" + if [ "$CURFN" = "call_host" ] || [ -z "$CURFN" ]; then FUNCTMP= else - FUNCTMP="${FUNCNAME[0]}" + FUNCTMP="$CURFN" fi # extra environment settings; set every time because commands are executed on host in subshell @@ -152,15 +235,32 @@ call_host(){ cat < "$CONTPIPE" return "$(cat < "$EXITPIPE")" } -export -f call_host +export_func call_host # from https://stackoverflow.com/questions/1203583/how-do-i-rename-a-bash-function copy_function() { - test -n "$(declare -f "$1")" || return - eval "${_/$1/$2}" - eval "export -f $2" + # portable retrieval of function source and re-definition under a new name + if is_zsh; then + # zsh: use `functions` to get the definition + if functions "$1" >/dev/null 2>&1; then + fnsrc="$(functions "$1" 2>/dev/null)" + else + return + fi + else + # bash: use declare -f + if declare -f "$1" >/dev/null 2>&1; then + fnsrc="$(declare -f "$1")" + else + return + fi + fi + # replace the function name in the source and eval it to create new function + fnnew="$(printf '%s\n' "$fnsrc" | sed "s/^$1\\b/$2/")" + eval "$fnnew" + export_func "$2" } -export -f copy_function +export_func copy_function if [ -z "$APPTAINER_ORIG" ]; then export APPTAINER_ORIG=$(which apptainer) @@ -198,11 +298,25 @@ apptainer(){ ) fi } -export -f apptainer +export_func apptainer # on host: get list of condor executables if [ -z "$APPTAINER_CONTAINER" ]; then - export APPTAINERENV_HOSTFNS=$(compgen -c | grep '^condor_\|^eos') + # portable command list discovery: + if command -v compgen >/dev/null 2>&1; then + export APPTAINERENV_HOSTFNS=$(compgen -c | grep -E '^condor_|^eos' | tr '\n' ' ') + else + # fallback: scan PATH for matching executables (portable) + APPTAINERENV_HOSTFNS="$( ( IFS=: + for d in $PATH; do + [ -d "$d" ] || continue + for f in "$d"/condor_* "$d"/eos*; do + [ -x "$f" ] && basename "$f" + done + done ) | sort -u | tr '\n' ' ')" + export APPTAINERENV_HOSTFNS + fi + if [ -n "$CALL_HOST_USERFNS" ]; then export APPTAINERENV_HOSTFNS="$APPTAINERENV_HOSTFNS $CALL_HOST_USERFNS" fi