Search code examples
bashloggingstack-tracequotingexpansion

Delayed expansion of composite variable in Bash


I'm defining a variable as a composition of other variables and some text, and I'm trying to get this variable to not expand its containing variables on the assigning. But I want it to expand when called later. That way I could reuse the same template to print different results as the inner variables keep changing. I'm truing to avoid eval as much as possible as I will be receiving some of the inner variables from third parties, and I do not know what to expect.

My use case, as below, is to have some "calling stack" so I can log all messages with the same format and keep a record of the script, function, and line of the logged message in some format like this: script.sh:this_function:42.

My attempted solution

called.sh:

#!/bin/bash

SCRIPT_NAME="`basename "${BASH_SOURCE[0]}"`"
CURR_STACK="${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"


echo "${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"
echo "${CURR_STACK}"
echo

function _func_1 {
    echo "${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"
    echo "${CURR_STACK}"
}
_func_1

So, I intend to get the same results while printing the "${CURR_STACK}" as when printing the previous line.

If there is some built-in or other clever way to log this 'call stack', by all means, let me know! I'll gladly wave my code good-bye, but I'd still like to know how to prevent the variables from expanding right away on the assigning of CURR_STACK, but still keep them able to expand further ahead.

Am I missing some shopt?

What I've tried:

Case 1 (expanding on line 4):

CURR_STACK="${SCRIPT_NAME}:${FUNNAME[0]}:${LINENO[0]}"
CURR_STACK="`echo "${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}"`"
CURR_STACK="`echo "\${SCRIPT_NAME}:\${FUNCNAME[0]}:\${LINENO[0]}"`"
called.sh::7              <------------------| These are control lines
called.sh::4  <---------------. .------------| With the results I expect to get.
                               X
called.sh:_func_1:12      <---´ `-------| Both indicate that the values expanded
called.sh::4  <-------------------------| on line 4 - when CURR_STACK was set.

Case 2 (not expanding at all):

CURR_STACK="\${SCRIPT_NAME}:\${FUNNAME[0]}:\${LINENO[0]}"
CURR_STACK=\${SCRIPT_NAME}:\${FUNCNAME[0]}:\${LINENO[0]}
CURR_STACK="`echo '${SCRIPT_NAME}:${FUNCNAME[0]}:${LINENO[0]}'`"
called.sh::7
${SCRIPT_NAME}:${FUNNAME[0]}:${LINENO[0]}  <-------.----| No expansion at all!...
                                                  / 
called.sh::12                                    /
${SCRIPT_NAME}:${FUNNAME[0]}:${LINENO[0]}  <----´

Solution

  • Shell variables are store plain inert text(*), not executable code; there isn't really any concept of delayed evaluation here. To make something that does something when used, create a function instead of a variable:

    print_curr_stack() {
        echo "$(basename "${BASH_SOURCE[1]}"):${FUNCNAME[1]}:${BASH_LINENO[0]}"
    }
    # ...
    echo "We are now at $(print_curr_stack)"
    # Or just run it directly:
    print_curr_stack
    

    Note: using BASH_SOURCE[1] and FUNCNAME[1] gets info about context the function was run from, rather than where it is in the function itself. But for some reason I'm not clear on, BASH_LINENO[1] gets the wrong info, and BASH_LINENO[0] is what you want.

    You could also write it to allow the caller to specify additional text to print:

    print_curr_stack() {
        echo "$@" "$(basename "${BASH_SOURCE[1]}"):${FUNCNAME[1]}:${BASH_LINENO[0]}"
    }
    # ...
    print_curr_stack "We are now at"
    

    (* There's an exception to what I said about variables just contain inert text: some variables -- like $LINENO, $RANDOM, etc -- are handled specially by the shell itself. But you can't create new ones like this except by modifying the shell itself.)