Search code examples
bashsolarisgnusubstitutionvariable-expansion

bash variable substitution not working on Solaris


I have this code snippet running on several Linux boxes, and a Solaris 10 box with bash 3.6 (iirc). However, on a Solaris 11 box, with GNU bash, version 4.4.11(1)-release (sparc-sun-solaris2.11) it gives the following error.

#!/bin/env bash
CLEAN_COUNT() {
    local L_STRING=$(sed '/[^[:graph:][:space:]]/{
        s/[^[:graph:][:space:]]//g; s/\[[0-9]*m//g; s/(B//g
        }' <<<$*) || return 1

    echo ${#L_STRING}
}

f() {
   ARGS=($@)
   echo $((${#ARGS[1]:-0} - $(CLEAN_COUNT ${ARGS[1]:-0}) ))
}

f one two three four

Error received: ./gather_data.bash: line 15: ${#ARGS[1]:-0} - $(CLEAN_COUNT ${ARGS[1]:-0}) : bad substitution

I've isolated the above code in it's own script, I've compared the shopt and set -o settings on that box with another one. I'm perplexed. If I can get the code to work without the substitution, even if ARGS has no element 1 and I'm running set -o nounset, then I will use another piece of code.


Solution

  • Changes affecting this happened in Bash 4.3 and Bash 4.4. Observe:

    • No error in Bash 4.2:

      $ docker run --rm -it bash:4.2 bash -u
      bash-4.2$ bash --version | head -n 1
      GNU bash, version 4.2.53(2)-release (x86_64-pc-linux-musl)
      bash-4.2$ declare -a var && echo "${#var[1]:-1}"
      0
      

      but this doesn't actually print my default value: var[1] is the empty string, hence 0. -u seems to ignore that var has no elements. There is no difference in behaviour between echo "${#var[1]:-1}", echo "${#var[1]}" and echo "${#var[1]}", they all print 0.

    • Bash 4.3 complains about unbound variable:

      $ docker run --rm -it bash:4.3 bash -u
      bash-4.3$ bash --version | head -n 1
      GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-musl)
      bash-4.3$ declare -a var && echo "${#var[1]:-1}"
      bash: var: unbound variable
      
    • Bash 4.4 complains about substitution:

      $ docker run --rm -it bash:4.4 bash -u
      bash-4.4$ bash --version | head -n 1
      GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-musl)
      bash-4.4$ declare -a var && echo "${#var[1]:-1}"
      bash: ${#var[1]:-1}: bad substitution
      

      even without set -u:

      bash-4.4# set +o nounset
      bash-4.4# declare -a var && echo "${#var[1]:-1}"
      bash: ${#var[1]:-1}: bad substitution
      

    Also, ${#var:-1} is considered "bad substitution" in all versions, even without set -u:

    $ for v in 3.2 4.{0..4}; do docker run --rm -it bash:$v; done
    bash-3.2# echo "${#var:-1}"
    bash: ${#var:-1}: bad substitution
    bash-3.2# exit
    exit
    bash-4.0# echo "${#var:-1}"
    bash: ${#var:-1}: bad substitution
    bash-4.0# exit
    exit
    bash-4.1# echo "${#var:-1}"
    bash: ${#var:-1}: bad substitution
    bash-4.1# exit
    exit
    bash-4.2# echo "${#var:-1}"
    bash: ${#var:-1}: bad substitution
    bash-4.2# exit
    exit
    bash-4.3# echo "${#var:-1}"
    bash: ${#var:-1}: bad substitution
    bash-4.3# exit
    exit
    bash-4.4# echo "${#var:-1}"
    bash: ${#var:-1}: bad substitution
    bash-4.4# exit
    exit
    

    I can't see any mention of changes to this behaviour in NEWS, but it seems to make sense, as ${#var[0]:-1} doesn't default to 1 anyway, so now the behaviour is consistent across scalars and arrays.

    This being said, I'd rewrite your function as follows:

    f () {
        local args=("$@")
        if [[ -z ${args[1]:-} ]]; then
            echo 0
        else
            echo $(( ${#args[1]} - $(clean_count "${args[1]}") ))
        fi
    }
    
    • Rename uppercase variable names to lowercase to avoid clash with shell and environment variables
    • Make args local to function
    • Quote "$@" in args to avoid splitting before assigning to array elements
    • Check if args[1] is the empty string, make sure no unset complaint is triggered with ${args[1]:-}
    • Treat cases for empty and non-empty string separately

    Alternatively, if f () is not a simplification and you never access elements other than what you show, you could further simplify to

    f () {
        if [[ -z ${2:-} ]]; then
            echo 0
        else
            echo $(( ${#2} - $(clean_count "$2") ))
        fi
    }