4

So i recently noticed that, when declaring a variable using a command substitution, let's say like so:

var=$(echo "this is running" > test; cat test)

Then it will run (the file test will be created) , even though i didn't yet called it (technically), which i would like so:

var=$(echo "this is running" > test; cat test)
echo "$var" # is where i would "normally" call the variable

How could I prevent the command substitution from actually running when declaring it in a variable, so it only actually run when I call the said variable?

PS: Well aware this is kind of a bad example, but it serve well to demonstrate what i mean, although with "useless use of cat" and "useless use of echo"...

2 Answers2

11

Sound like you want a variable whose contents is dynamically generated.

bash does not have support for ksh93's disciplines, or zsh's dynamic named directory or mksh's value substitution which would make it easier, but you could use this kind of hack, using namerefs:

var_generator() { date --iso-8601=ns; }
var_history=()
typeset -n var='var_history[
  ${##${var_history[${#var_history[@]}]=$(var_generator)}},${#var_history[@]}-1
]'

Here with var defined as a reference to an element of the $var_history array, using the fact that array indices are evaluated dynamically and allow running arbitrary code (here used to run the var_generator function and assign its output to a new element of the array).

Then:

bash-5.1$ echo "$var"
2021-03-23T13:36:43,243211696+00:00
bash-5.1$ echo "$var"
2021-03-23T13:36:45,517726619+00:00

That sounds a bit too convoluted though where you could just use $(var_generator) here. One advantage though is that you can still do things like ${var#pattern} while bash (contrary to zsh) won't let you do ${$(cmd)#pattern}.

  • In your example, can i just replace the var_generator by the actual command, or I'm i obliged to put the command i want to use in a command substitution inside a function? (like you did with var_generator) – Nordine Lotfi Mar 23 '21 at 14:27
  • 1
    @NordineLotfi, yes, you can put the command directly in there without using an intermediary function. Beware of quoting though. – Stéphane Chazelas Mar 23 '21 at 14:32
  • Is there a version of this trick for zsh, too? – HappyFace Aug 10 '21 at 10:31
1

It runs because you did call it. Shell expansions happen right there, unlike e.g. variables in Make, which usually are lazily evaluated. If you did var='$(...)', the command substitution wouldn't run, but it would be difficult to run it later, either. This is similar to how something foo=$bar copies the value of bar at that moment, instead of looking at it later when foo is eventually used.

The simple alternative is to use a function instead. E.g. this should print two dates that are around 5 seconds apart:

getdate() {
    date +"%F %T"
}
a=$(getdate)
sleep 5
b=$(getdate)
echo "$a $b"

The downside of using a function is that the string operations you can use with parameter expansion aren't available. You can't do ${(getdate)% *} to get just the first part of the output, you'll have to do foo=$(getdate); foo=${foo% *};

ilkkachu
  • 138,973