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

12
  • 7
    Try using a function instead.
    – FelixJN
    Mar 23, 2021 at 12:48
  • 1
  • 4
    What you are dealing with is a string that represents a piece of shell code that you want to run later. This is the typical use for functions.
    – Kusalananda
    Mar 23, 2021 at 12:53
  • 2
  • 2
    @FelixJN, bash variables can be string or array or associative array or namerefs. There are special variables like $SECONDS, $RANDOM, $PWD... whose contents is automatically generated. You can also set the integer attribute on a variable which causes its contents to be interpreted as arithmetic expression (though in the case of bash, that's done upon assignment only after the attribute has been set; still, see a='b[$(uname>&2)]' bash -c 'var=$a; typeset -i var; var=var+1' for instance). Mar 23, 2021 at 14:39

2 Answers 2

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

3
  • 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) Mar 23, 2021 at 14:27
  • 1
    @NordineLotfi, yes, you can put the command directly in there without using an intermediary function. Beware of quoting though. Mar 23, 2021 at 14:32
  • Is there a version of this trick for zsh, too?
    – HappyFace
    Aug 10, 2021 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% *};

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .