Ubuntu: How do I assign a variable in Bash whose name is expanded ($) from another variable?



Question:

I have a part of script as below.

notify() {  printf -v "old_notif" "%d" "$notif_$2"  now=$(date +%s)  fark=$((now - old_notif))  echo $old_notif  if [ -z $old_notif ]; then    

.....

x1=$(date +%s)  export "notif_$2=$x1"  

I call notify with 2 parameters.

notify xyz klm  

I create a dynamic variable in function with export. And the main script is in the while loop. My question is how can I use notif_$2 variable in if check? Or how can I assign it's decimal content to another variable? In the example I tried with printf it doesn't assign the content.


Solution:1

TL;DR: One way is local tmp="notif_$2"; printf -v "old_notif" "%d" "${!tmp}".

You are trying to expand a parameter (in this case, a variable) whose name must itself be obtained by expanding another parameter (in this case, a positional parameter). Furthermore, the positional parameter must be expanded, then concatenated with the text notif_, to produce the text that must then itself be expanded.

Bash lets you do this cleanly with indirect expansion or a nameref.

The first two sections of this post show drop-in replacements for your printf -v command, using those techniques. The remaining sections are optional; they further explain these features and what you can do with them.

Way 1: Indirect Expansion

Indirect expansion is the most similar, conceptually speaking, to what you have written. Instead of the printf -v command you have now, you can use these two commands:

local tmp="notif_$2"  printf -v "old_notif" "%d" "${!tmp}"  

local makes the tmp parameter local to the shell function. If for some reason you don't want that, just omit the word local. The parameter doesn't need to be called tmp.

Indirect expansion is covered in 3.5.3 Shell Parameter Expansion in the Bash reference manual.

Way 2: Namerefs

Another way is to use a nameref. A nameref refers to a parameter. You create it from the name of the parameter, but once created, it behaves as though it is that parameter when you read from or write to it.

To use a nameref, replace your printf -v command with:

local -n ref="notif_$2"  printf -v "old_notif" "%d" "$ref"  

Passing the -n option to local or declare causes the newly introduced parameter to be a nameref. Notice that, in the printf command, you will use ordinary parameter expansion ($ref) -- not indirect expansion -- because the shell performs the indirection automatically for the nameref.

You cannot omit local here, though in the (unusual) case where you wanted the nameref to be usable after the function has returned, you could replace it with declare.

(Depending on your needs, it might even make sense to use a nameref for more than just these two lines. To learn how, read on.)

Namerefs are covered in 3.4 Shell Parameters in the Bash reference manual.

Detailed Explanation: How Indirect Expansion Works

Shell features are most easily demonstrated through interactive use, but positional parameters (like 2, expanded via $2) don't have quite the same meaning outside a shell function, and the local builtin doesn't work at all outside a function (you would use declare instead, or nothing).

So, to start with a simplified example, suppose you are working interactively in your shell and you have run:

x=foo  export "notif_$x=1234"  

After you run that, 1234 is stored in the parameter notif_foo. (It also exports that parameter as an environment variable.) We can see this by inspecting notif_foo:

echo "$notif_foo"  

This outputs 1234.

Your scenario is analogous to not knowing what is in the x parameter. (In your case, you instead don't know what's in the second positional parameter passed to your shell function. I'll get to that soon.)

But you can build the parameter name and put it in another parameter:

y="notif_$x"  

Now y holds notif_foo, so you can use indirect expansion on y, which looks like this:

"${!y}"  

That expands to 1234, just as if you had used "$notif_foo". But you don't need to know $x is foo to use it.

For example, this will assign 1234 to old_notif:

old_notif="${!y}"  

If you need to format the contents of $notif_foo, you can do that, too. For example, you can use printf if you need it. This command is similar to the printf command in your question, and has the effect of assigning 1234 to old_notif:

printf -v old_notif '%d' "${!y}"  

(It also works in your original quoting style, i.e., printf -v "old_notif" "%d" "${!y}" has the same effect.)

Of course, this relies on the y parameter being suitably assigned first.

To write your shell function, you will replace $x with $2, and you will probably want to use the local builtin to prevent your temporary variable--which I will now call tmp instead of y--from leaking out of the function's scope.

local tmp="notif_$2"  printf -v old_notif '%d' "${!tmp}"  

Or, using the quoting style you used in the question:

local tmp="notif_$2"  printf -v "old_notif" "%d" "${!tmp}"  

Detailed Explanation: How Namerefs Work

To try out namrefs interactively, you must use declare -n rather than local -n (because local only works--or is needed--inside the body of a shell function).

As before, suppose you have run:

x=foo  export "notif_$x=1234"  

Thus $notif_foo expands to 1234. You can create a nameref to the parameter named by the result of expanding "notif_$x":

declare -n y="notif_$x"  

Now y refers to the name notif_foo, and ordinary parameter expansion on y will automatically dereference that name, thereby expanding the notif_foo parameter. That is, this expands to 1234, just as if you had used $notif_foo:

"$y"  

To write your shell function, you will replace $x with $2, and I recommend using local instead of declare. I suggest also using a somewhat more descriptive name than y; for a short function with no other nameref declarations, ref is probably adequately clear.

local -n ref="notif_$2"  print -v old_notif '%d' "$ref"  

Or, with the quoting style you've been using:

local -n ref="notif_$2"  print -v "old_notif" "%d" "$ref"  

Namerefs are Powerful

A nameref lets you do more with its referred-to parameter than just read from it. You can also, for example, write to it:

x=foo  declare -n y="notif_$x"  y=1234  

The second command creates a nameref to a parameter that might not yet exist. That's no problem! It will be created when you first assign to it, even if that assignment is through the nameref.

The third command looks like it assigns 1234 to y, but really it assigns it to notif_foo. Now both $y and $notif_foo expand to 1234. $notif_foo expands to 1234 because that's the value stored in notif_foo; $y expands to 1234 because y is a nameref for notif_foo.

Suppose you want to know what y refers to, though. That is, suppose you want to get notif_foo, rather than 1234, from y. Well, you can, because with a nameref, indirect expansion has the opposite of its usual effect. This expands to notif_foo:

"${!y}"  

In your function, you could introduce notif_$2 through a nameref.

This suggests another way to deal with notif_$2 in your function: you can introduce it through a nameref, and use the nameref for each subsequent access.

Currently you have:

x1=$(date +%s)  export "notif_$2=$x1"  

As an alternative, you could use:

x1=$(date +%s)  local -n ref="notif_$2"  ref="$x1"  export "${!ref}=$ref"  

That's more complicated that necessary, though, since presumably you only created the x1 parameter because export "notif_$2=$(date +%s)" is hard to read. ref="$(date +%s)" is just as simple, though, so you can omit the x1= line and write:

local -n ref="notif_$2"  ref="$(date +%s)"  export "${!ref}=$ref"  

It occurs to me that you might have been using export just to assign the parameter for use in your shell. If you don't actually need to export it to child processes, then you can just use the first two lines, and that's simpler than what you have.

If you do need to export it, use all three. It's still a bit more complicated than what you have... but it may let you simplify the rest of your function, because, afterwards:

  • Instead of having to write notif_$2, you can just write ref.
  • This works even in scenarios where writing notif_$2 is inadequate. That is, it is no longer necessary to do anything special (like Way 1 and Way 2 above) to expand the parameter whose name is the result of expanding notif_$2. Just write ref.
  • This even works for writing -- and creating, and unsetting -- the parameter. (After the point of declaration, ref=text writes to and can even create the referred-to parameter; unset ref unsets the referred-to parameter.)

If you're going to use a nameref throughout your whole function, you may want to think of a more meaningful name for it than ref. (The best name will, of course, be determined by the task you are writing the function to carry out.)


Note:If u also have question or solution just comment us below or mail us on toontricks1994@gmail.com
Previous
Next Post »