17

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?

User1
  • 39,458
  • 69
  • 187
  • 265
  • I'm curious why you want to do this or what it is you're actually trying to accomplish. Sometimes `eval` is the right or only way to go, but there are also special features of `declare` and `printf` that might be useful. And there may be other ways to accomplish what you're after. – Dennis Williamson May 24 '10 at 23:28
  • I have a bash script that I want to be configurable. I want the user to have the ability to specify a "pattern". Later, some variables in the pattern will be replaced with activities run by the script (SQL queries, SOAP calls, and other in-house utilities) and passed to another command-line program. I'm somewhat new to Bash and something about this approach just feels wrong. Thanks for asking more details. – User1 May 25 '10 at 03:07

3 Answers3

10

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
tokland
  • 66,169
  • 13
  • 144
  • 170
8

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"
gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
  • 1
    This is the answer I was going to write if it hadn't already been here. (Then again, I only spotted this question at all by virtue of the answer having been added, bouncing it back to the front page). :) – Charles Duffy Feb 23 '18 at 22:47
0

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."

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 1
    The answer matches the example of the question. But it does not seem to be very useful for anything more complicated that basic string replacements. If I try to follow this approach, I have to write a Bash interpreter in Bash. – ceving Oct 21 '14 at 08:49
  • My answer is a way to insulate the script from the untrusted input of a user. It's based on the OP's comment attached to the question in response to my query. – Dennis Williamson Feb 22 '18 at 23:38