bashsubshell

Bash Subshell Expansion as Parameter to Function


I have a bash function that looks like this:

banner(){
    someParam=$1
    someOtherParam=$2

    precedingParams=2
    for i in $(seq 1 $precedingParams);do
        shift
    done

    for i in $(seq 1 $(($(echo ${#@}) - $precedingParams)));do
         quotedStr=$1
         shift
         #do some stuff with quotedStr
    done

}

This function, while not entirely relevant, will build a banner. All params, after the initial 2, are quoted lines of text which can contain spaces. The function fits each quoted string within the bounds of the banner making new lines where it sees fit. However, each new parameter ensures a new line

My function works great and does what's expected, the problem, however, is in calling the function with dynamic parameters as shown below:

e.g. of call with standard static parameters:

 banner 50 true "this is banner text and it will be properly fit within the bounds of the banner" "this is another line of banner text that will be forced to be brought onto a new line"

e.g. of call with dynamic parameter:

 banner 50 true "This is the default text in banner" "$([ "$someBool" = "true" ] && echo "Some text that should only show up if bool is true")"

The problem is that if someBool is false, my function will still register the resulting "" as a param and create a new empty line in the banner.

As I'm writing this, I'm finding the solution obvious. I just need to check if -n $quotedStr before continuing in the function.

But, just out of blatant curiosity, why does bash behave this way (what I mean by this is, what is the process through which subshell expansion occurs in relation to parameter isolation to function calls based on quoted strings)

The reason I ask is because I have also tried the following to no avail:

banner 50 true "default str" $([ "$someBool" = "true" ] && echo \"h h h h\")

Thinking it would only bring the quotes down if someBool is true. Indeed this is what happens, however, it doesn't properly capture the quoted string as one parameter.

Instead the function identifies the following parameters:

default str
"h
h
h
h"

When what I really want is:

default str
h h h h

I have tried so many different iterations of calls, again to no avail:

$([ "$someBool" = "true" ] && echo "h h h h")
$([ "$someBool" = "true" ] && echo \\\"h h h h\\\")
$([ "$someBool" = "true" ] && awk 'BEGIN{printf "%ch h h h h%c",34,34}')

All of which result in similar output as described above, never treating the expansion as a true quoted string parameter.


Solution

  • The reason making the command output quotes and/or escapes doesn't work is that command substitutions (like variable substitutions) treat the result as data, not as shell code, so shell syntax (quotes, escapes, shell operators, redirects, etc) aren't parsed. If it's double-quoted it's not parsed at all, and if it's not in double-quotes, it's subject to word splitting and wildcard expansion.

    So double-quotes = no word splitting = no elimination of empty string, and no-double-quotes = word splitting without quote/escape interpretation. (You could do unpleasant things to IFS to semi-disable word splitting, but that's a horrible kluge and can cause other problems.)

    Usually, the cleanest way to do things like this is to build a list of conditional arguments (or maybe all arguments) in an array:

    bannerlines=("This is the default text in banner")    # Parens make this an array
    [ "$someBool" = "true" ] &&
        bannerlines+=("Some text that should only show up if bool is true")
    banner 50 true "${bannerlines[@]}"
    

    The combination of double-quotes and [@] prevents word-splitting, but makes bash expand each array element as a separate item, which is what you want. Note that if the array has zero elements, this'll expand to zero arguments (but be aware that an empty array, like bannerlines=() is different from an array with an empty element, like bannerlines=("")).