0

I made a function in bash and when I call it, it crashes with an unbound variable error. I don't understand cause the variables that are said to be unbound are declared. Moreover, it seems to be triggered randomly like some times it crashes on line 66, some times it crashes on line 76 and some other times it crashes on line 86.

Here is the function:

#!/usr/bin/env bash

function setConfigLS() {
    declare DFLT_CFG_FILE="${WEB_DOCUMENT_ROOT}/application/config/config.php"
    declare DFLT_ARRAY='config'
    declare cfgFile="$DFLT_CFG_FILE"
    declare array="$DFLT_ARRAY"
    declare value key arg
    declare -a args=()

    while (( $# > 0 )); do
        arg="$1" && shift
        case "$arg" in
            --file=*)
                cfgFile="${arg#*=}"
            ;;
            -f|--file)
                cfgFile="$1"
                shift
            ;;
            --value=*)
                value="${arg#*=}"
            ;;
            -v|--value)
                value="$1"
                shift
            ;;
            --key=*)
                key="${arg#*=}"
            ;;
            -k|--key)
                key="$1"
                shift
            ;;
            --array=*)
                array="${arg#*=}"
            ;;
            -a|--array)
                array="$1"
                shift
            ;;
            -h|--help)
                echo >&2 'Set a LimeSurvey configuration option.'
                echo >&2 ''
                echo >&2 'Usage:'
                echo >&2 '  setConfigLS [options...] <KEY> <VALUE>'
                echo >&2 '  setConfigLS [options...] --value=<VALUE> --key=<KEY>'
                echo >&2 ''
                echo >&2 'Options:'
                echo >&2 '  --file, -f <CONFIG_FILE>  LimeSurvey configuration file.'
                echo >&2 "                              Default: ${DFLT_CFG_FILE}"
                echo >&2 '  --array, -a <ARRAY>       Name of array containing the configuration.'
                echo >&2 "                              Default: ${DFLT_ARRAY}"
                echo >&2 '  --key, --k <KEY>          Key of the configuration option to set. (required)'
                echo >&2 '  --value, -v <VALUE>       Value of the configuration option. (required)'
                echo >&2 '  --help, -h                Prints this message.'
                echo >&2 ''
                return 0
            ;;
            *)
                args+=( "$arg" )
            ;;
        esac
    done

    if [ -z "$key" ]; then # line 66: key: unbound variable
        if (( ${#args} > 0 )); then
            key="${args[0]}"
            args=( "${args[@]:1}" )
        else
            echo 'Error: `--key` is required' >&2
            return 1
        fi
    fi

    if [ -z "$value" ]; then # line 76: value: unbound variable
        if (( ${#args} > 0 )); then
            value="${args[0]}"
            args=( "${args[@]:1}" )
        else
            echo 'Error: `--value` is required' >&2
            return 1
        fi
    fi

    if (( ${#args} > 0 )); then # line 86: args: unbound variable
        echo 'Error: too many arguments' >&2
        return 1
    fi

    array="${array//\//\\\/}"
    value="${value//$'\n'/\\$'\n'}"

    ssed -Ri "$cfgFile" \
        -e 's~^(\s*)('"${array}"'\s*=>\s*array\s*\()((?:\([^)]*\)|[^)])+)~\1\2\n\1    \3\n\1~'

    ssed -Ri "$cfgFile" \
        -e '/^\s*'"${array}"'\s*=>\s*array\s*\([^)]*$/ {
                :a
                n
                s~^((?:\s*(?:[^,/\s]|/[^/]))+)(\s*//.*)?$~\1,\2~
                s~^(\s*)//\s*('"${key//~/\\~}"'\s*=>)~\1\2~
                /^\s*\)/ {
                    i \        '"${key}"'=>'"${value}"',
                    bq
                }
                /^\s*'"${key//\//\\\/}"'\s*=>/ {
                    s~>.*~>'"${value//~/\\~}"',~
                    bq
                }
                ba
                :q
            }'
}

I tried replacing declare value key arg to...

declare value=
declare key=
declare arg=

...but it didn't change anthing.

I'm a little bit confused! Did I miss something? Is there something I'm not seeing?


Edit 1

The function is called from an entrypoint script of a docker image based on ubuntu 18.04. In fact, I use this image.

The function's file is copied to /opt/docker/functions/set-config-ls.sh.

Here is the script from which the function is called:

#!/usr/bin/env bash
set -eu

declare FUNC_DIR='/opt/docker/functions'
declare APP_DIR="${WEB_DOCUMENT_ROOT}"
declare DB_SETUP_PHP="/opt/docker/db_setup.php"

source "${FUNC_DIR}/tty-loggers.sh"
source "${FUNC_DIR}/yes-no.sh"
source "${FUNC_DIR}/file-env.sh"
source "${FUNC_DIR}/set-config-ls.sh"
source "${FUNC_DIR}/env-list-vars.sh"


####################################################################
########################## Setup Variables #########################

fileEnv 'LIMESURVEY_DB_TYPE' 'mysql'
fileEnv 'LIMESURVEY_DB_HOST' 'mysql'
fileEnv 'LIMESURVEY_DB_PORT' '3306'
fileEnv 'LIMESURVEY_TABLE_PREFIX' ''
fileEnv 'LIMESURVEY_ADMIN_NAME' 'Lime Administrator'
fileEnv 'LIMESURVEY_ADMIN_EMAIL' 'lime@lime.lime'
fileEnv 'LIMESURVEY_ADMIN_USER' ''
fileEnv 'LIMESURVEY_ADMIN_PASSWORD' ''
fileEnv 'LIMESURVEY_DEBUG' '0'
fileEnv 'LIMESURVEY_SQL_DEBUG' '0'
fileEnv 'MYSQL_SSL_CA' ''
fileEnv 'LIMESURVEY_USE_INNODB' ''

# if we're linked to MySQL and thus have credentials already, let's use them
fileEnv 'LIMESURVEY_DB_NAME' "${MYSQL_ENV_MYSQL_DATABASE:-limesurvey}"
fileEnv 'LIMESURVEY_DB_USER' "${MYSQL_ENV_MYSQL_USER:-root}"

if [ "${LIMESURVEY_DB_USER}" = 'root' ]; then
    fileEnv 'LIMESURVEY_DB_PASSWORD' "${MYSQL_ENV_MYSQL_ROOT_PASSWORD:-}"
else
    fileEnv 'LIMESURVEY_DB_PASSWORD' "${MYSQL_ENV_MYSQL_PASSWORD:-}"
fi

if [ -z "${LIMESURVEY_DB_PASSWORD}" ]; then
    logError 'error: missing required LIMESURVEY_DB_PASSWORD environment variable' >&2
    logError '  Did you forget to -e LIMESURVEY_DB_PASSWORD=... ?' >&2
    logError '' >&2
    logError '  (Also of interest might be LIMESURVEY_DB_USER and LIMESURVEY_DB_NAME.)' >&2
    exit 1
fi

declare -A CONNECTION_STRINGS=(
    [mysql]="mysql:host=${LIMESURVEY_DB_HOST};port=${LIMESURVEY_DB_PORT};dbname=${LIMESURVEY_DB_NAME};"
    [dblib]="dblib:host=${LIMESURVEY_DB_HOST};dbname=${LIMESURVEY_DB_NAME}"
    [pgsql]="pgsql:host=${LIMESURVEY_DB_HOST};port=${LIMESURVEY_DB_PORT};user=${LIMESURVEY_DB_USER};password=${LIMESURVEY_DB_PASSWORD};dbname=${LIMESURVEY_DB_NAME};"
    [sqlsrv]="sqlsrv:Server=${LIMESURVEY_DB_HOST};Database=${LIMESURVEY_DB_NAME}"
)

if [ -z "${CONNECTION_STRINGS[${LIMESURVEY_DB_TYPE}]}" ]; then
    logError "error: invalid database type: ${LIMESURVEY_DB_TYPE}" >&2
    logError "  LIMESURVEY_DB_TYPE must be either \"mysql\", \"dblib\", \"pgsql\" or \"sqlsrv\"." >&2
    exit 1
fi


####################################################################
######################## Download LimeSurvey #######################

if [ ! -f "${APP_DIR}/.RELEASE_${LIMESURVEY_GIT_RELEASE}" ] || isYes "${LIMESURVEY_FORCE_FETCH}"; then
    find "$APP_DIR" -maxdepth 1 -type f -name '.RELEASE_*' -delete

    logInfo "Retrieving LimeSurvey... (this operation may take a while)" >&2
    wget -O "/tmp/lime.tar.gz" \
        --progress="$( [ -t 1 ] && echo 'bar:noscroll' || echo 'dot:mega' )" \
        "https://github.com/LimeSurvey/LimeSurvey/archive/${LIMESURVEY_GIT_RELEASE}.tar.gz"


    logInfo "Extracting files from archive..." >&2
    tar -xzf "/tmp/lime.tar.gz" \
        --strip-components=1 \
        --keep-newer-files \
        --exclude-vcs \
        --to-command='sh -c '\''
            mkdir -p "$(dirname "'"${APP_DIR}"'/$TAR_FILENAME")" &&
                touch "'"${APP_DIR}"'/$TAR_FILENAME" &&
                dd of="'"${APP_DIR}"'/$TAR_FILENAME" >/dev/null 2>&1 &&
                echo "'"${APP_DIR}"'/$TAR_FILENAME" '\' |
        xargs -I '{}' touch -t 195001010000 '{}'

    chown -R "${APPLICATION_USER}:${APPLICATION_GROUP}" "$APP_DIR"
    rm "/tmp/lime.tar.gz"

    touch ".RELEASE_${LIMESURVEY_GIT_RELEASE}"
fi


####################################################################
######################### LimeSurvey Setup #########################

# Install BaltimoreCyberTrustRoot.crt.pem
if [ ! -f "${APP_DIR}/BaltimoreCyberTrustRoot.crt.pem" ]; then
    logInfo "Downloading BaltimoreCyberTrustroot.crt.pem..."
    curl -fsSLo "${APP_DIR}/BaltimoreCyberTrustRoot.crt.pem" \
        "https://www.digicert.com/CACerts/BaltimoreCyberTrustRoot.crt.pem"
fi

if [ ! -f "${APP_DIR}/application/config/config.php" ]; then
    logWarn "No config file for LimeSurvey"
    logWarn "  Copying default config file..."
    # Copy default config file but also allow for the addition of attributes
    echo "            'attributes' => array()," |
        awk '/lime_/ && c == 0 { c = 1; system("cat") } { print }' \
            "${APP_DIR}/application/config/config-sample-${LIMESURVEY_DB_TYPE}.php" \
            > "${APP_DIR}/application/config/config.php"
fi

# Set LimeSurvey configs
setConfigLS -a 'db' -k 'connectionString' "'${CONNECTION_STRINGS[${LIMESURVEY_DB_TYPE}]}'"
setConfigLS -a 'db' -k 'tablePrefix' "'${LIMESURVEY_TABLE_PREFIX}'"
setConfigLS -a 'db' -k 'username' "'${LIMESURVEY_DB_USER}'"
setConfigLS -a 'db' -k 'password' "'${LIMESURVEY_DB_PASSWORD}'"
setConfigLS -a 'urlManager' -k 'urlFormat' "'path'"
setConfigLS -k 'debug' "${LIMESURVEY_DEBUG}"
setConfigLS -k 'debugsql' "${LIMESURVEY_SQL_DEBUG}"

if [ -n "${MYSQL_SSL_CA}" ]; then
    setConfigLS -a 'db' 'attributes' \
        "array(PDO::MYSQL_ATTR_SSL_CA => '${APP_DIR//\//\\\/}\/${MYSQL_SSL_CA}',
            PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false)"
fi

declare cfg key val
for ENV_VAR in $(envListVars "limesurvey\."); do
        val="$(envGetValue "$ENV_VAR")"
        cfg="${ENV_VAR#limesurvey.}"
        cfg="${cfg%%.*}"
        key="${ENV_VAR#limesurvey.*.}"
        setConfigLS -a "$cfg" "$key" "$val"
done

mkdir -p "${APP_DIR}/upload/surveys"
chown -R "${APPLICATION_USER}:${APPLICATION_GROUP}" \
    "${APP_DIR}/tmp" "${APP_DIR}/upload" "${APP_DIR}/application/config"

####################################################################
#################### LimeSurvey Database Setup #####################

if [ -n "${LIMESURVEY_USE_INNODB}" ]; then
    # If you want to use INNODB - remove MyISAM specification from LimeSurvey code
    sed -i "/ENGINE=MyISAM/s/\(ENGINE=MyISAM \)//1" \
        "${APP_DIR}/application/core/db/MysqlSchema.php"
fi

logInfo "Waiting for database..." >&2
while ! curl -sL "${LIMESURVEY_DB_HOST}:${LIMESURVEY_DB_PORT:-3306}"; do sleep 1; done

DBSTATUS=$(TERM=dumb php -f "$DB_SETUP_PHP" -- \
    "${LIMESURVEY_DB_HOST}" "${LIMESURVEY_DB_USER}" "${LIMESURVEY_DB_PASSWORD}" \
    "${LIMESURVEY_DB_NAME}" "${LIMESURVEY_TABLE_PREFIX}" "${MYSQL_SSL_CA}" \
    "${APP_DIR}") &>/dev/null

if [ "${DBSTATUS}" != "DBEXISTS" ] &&  [ -n "${LIMESURVEY_ADMIN_USER}" ] && [ -n "${LIMESURVEY_ADMIN_PASSWORD}" ]; then
    logInfo 'Database not yet populated - installing Limesurvey database' >&2
    su - "${APPLICATION_USER}" \
        -c php -f "${APP_DIR}/application/commands/console.php" -- \
            install "${LIMESURVEY_ADMIN_USER}" "${LIMESURVEY_ADMIN_PASSWORD}" \
            "${LIMESURVEY_ADMIN_NAME}" "${LIMESURVEY_ADMIN_EMAIL}" verbose
fi

if [ -f "${APP_DIR}/application/commands/UpdateDbCommand.php" ]; then
    logInfo 'Updating database...' >&2
    su - "${APPLICATION_USER}" -c php "${APP_DIR}/application/commands/console.php" updatedb
else
    logWarn 'WARNING: Manual database update may be required!' >&2
fi

if [ -n "${LIMESURVEY_ADMIN_USER}" ] && [ -n "${LIMESURVEY_ADMIN_PASSWORD}" ]; then
    logInfo 'Updating password for admin user...' >&2
    su - "${APPLICATION_USER}" \
        -c php -f "${APP_DIR}/application/commands/console.php" -- \
            resetpassword "${LIMESURVEY_ADMIN_USER}" "${LIMESURVEY_ADMIN_PASSWORD}"
fi

Here is the output of bash --version:

GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Edit 2

I put what I could on github. Here is the commit.

I'm not entirely sure but I think it should work if you clone the repo and run the launch script.

  • Can you provide a shorter example that triggers your behavior? How do you call your script when the error happens? – undercat applauds Monica Dec 23 '19 at 15:38
  • Also, what Bash version are you using? – undercat applauds Monica Dec 23 '19 at 15:39
  • @steeldriver Check the comments. I wrote a comment on each line where it crashes with the error message. – Elie G. Dec 23 '19 at 16:21
3

This doesn't set a value for any of value, key or arg:

declare value key arg

So, if the assignment to key in the case isn't reached:

while (( $# > 0 )); do
    arg="$1" && shift
    case "$arg" in
        --key=*)
            key="${arg#*=}"
        ;;

then key will still be unset ("unbound") after the loop, and since the script has set -u, it'll throw an error when it's used.

if [ -z "$key" ]; then # line 66: key: unbound variable

Initializing the variables to empty strings (as with declare key= value= arg=) would remove that issue.

However, you also have this reference to args:

if [ -z "$key" ]; then # line 66: key: unbound variable
    if (( ${#args} > 0 )); then

Note that that refers to args, not args[@], you're taking the length of the zeroth element of the array args, not the number of elements in it. But if args is empty, that zeroth element doesn't exist, again, an error.

|improve this answer|||||
  • Thanks a lot! I had tried to initialize value, key and arg but I was still getting an error on args and I didn't understand why. I never realized I forgot the [@]. Thanks again! – Elie G. Dec 23 '19 at 16:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.