User1
User1

Reputation: 41251

Lazy Evaluation in Bash

Is there more elegant way of doing lazy evaluation than the following:

pattern='$x and $y'
x=1
y=2
eval "echo $pattern"

results:

1 and 2

It works but eval "echo ..." just feels sloppy and may be insecure in some way. Is there a better way to do this in Bash?

Upvotes: 17

Views: 14500

Answers (4)

John Sherwood
John Sherwood

Reputation: 1508

I know this question was from a long time ago, but the information was useful to me recently, so here is what I found.

This is actually much more straightforward now within bash. In version 4.4 a new feature called "Parameter Transformation" was introduced that solves this problem directly.

With parameter transformation, a number of transformations are possible by appending @{letter-code} to the variable name within the curly braces (For example: ${MYVAR@P}). The letter-code you use determines which transformation you wish to use, and the transformation associated with the letter-code P is prompt-string transformation. (For details on other transformations, consult the bash online documentation on parameter expansion.)

What that means is that any string that could previously be evaluated within your $PS1 prompt variable can also be evaluated within your regular variables by using this transformation. Here are some examples:

# First example: Not extremely useful, since neither value changes after login
bash> USERATHOST='\u@\h'

bash> echo ${USERATHOST}
\u@\h

bash> echo ${USERATHOST@P}
root@mysystem

# More interesting example: Dynamically adjusting a variable based on changes in OS.
# Using single-quotes or escaping $-chars inside double quotes, expansion is delayed.
bash> REALJAVAHOME='$(dirname $(dirname $(realpath $(which java))))'

bash> echo ${REALJAVAHOME}
$(dirname $(dirname $(realpath $(which java))))

bash> echo ${REALJAVAHOME@P}
/usr/lib/jvm/java-17-openjdk-amd64

bash> echo 2 | update-alternatives --config java &>/dev/null

bash> echo ${REALJAVAHOME@P}
/usr/lib/jvm/java-11-openjdk-amd64

bash> echo 3 | update-alternatives --config java &>/dev/null

bash> echo ${REALJAVAHOME@P}
/usr/lib/jvm/java-17-openjdk-amd64

Sadly this didn't solve my problem, which was trying to find a way to make $JAVA_HOME itself dynamically reflect the JDK folder matching the java binary currently set by update-alternatives. However, for any situation where you can control how the variable is being called (i.e., using it in your own scripts), this works like a charm.

Upvotes: 2

tokland
tokland

Reputation: 67900

You can use the command envsubst from gettext, for example:

$ pattern='x=$x and y=$y'
$ x=1 y=2 envsubst <<< $pattern
x=1 and y=2

Upvotes: 11

gniourf_gniourf
gniourf_gniourf

Reputation: 46883

One safe possibility is to use a function:

expand_pattern() {
    pattern="$x and $y"
}

That's all. Then use as follows:

x=1 y=1
expand_pattern
echo "$pattern"

You can even use x and y as environment variables (so that they are not set in the main scope):

x=1 y=1 expand_pattern
echo "$pattern"

Upvotes: 9

Dennis Williamson
Dennis Williamson

Reputation: 360535

You're right, eval is a security risk in this case. Here is one possible approach:

pattern='The $a is $b when the $z is $x $c $g.'    # simulated input from user (use "read")
unset results
for word in $pattern
do
    case $word in
        \$a)
            results+=($(some_command))   # add output of some_command to array (output is "werewolf"
            ;;
        \$b)
            results+=($(echo "active"))
            ;;
        \$c)
            results+=($(echo "and"))
            ;;
        \$g)
            results+=($(echo "the sky is clear"))
            ;;
        \$x)
            results+=($(echo "full"))
            ;;
        \$z)
            results+=($(echo "moon"))
            ;;
          *)
            do_something    # count the non-vars, do a no-op, twiddle thumbs
            # perhaps even sanitize %placeholders, terminal control characters, other unwanted stuff that the user might try to slip in
            ;;
    esac
done
pattern=${pattern//\$[abcgxz]/%s}    # replace the vars with printf string placeholders
printf "$pattern\n" "${results[@]}"  # output the values of the vars using the pattern
printf -v sentence "$pattern\n" "${results[@]}"  # put it into a variable called "sentence" instead of actually printing it

The output would be "The werewolf is active when the moon is full and the sky is clear." The very same program, if the pattern is 'The $x $z is out $c $g, so the $a must be $b.' then the output would be "The full moon is out and the sky is clear, so the werewolf must be active."

Upvotes: 0

Related Questions