Bash namerefs for dynamic variable referencing

Redowan Delowar September 20, 2024
Source

While going through a script at work today, I came across Bash's nameref feature. It uses declare -n ref="$1" to set up a variable that allows you to reference another variable by name - kind of like pass-by-reference in C. I'm pretty sure I've seen it before, but I probably just skimmed over it.

As I dug into the man pages, I realized there's a gap in my understanding of how variable references actually work in Bash - probably because I never gave it proper attention and just got by cobbling together scripts.

Namerefs

By default, Bash variables are global unless declared as local within a function. However, when you pass variables as arguments to a function, they are accessed via positional parameters like $1, $2, etc., and any changes to these parameters inside the function do not affect the original variables outside the function.

Namerefs allow you to essentially define a pointer to another variable. By creating a nameref, you can indirectly reference and manipulate the target variable without knowing its name beforehand. This is incredibly useful for writing generic functions that can operate on different variables based on input parameters.

Basic usage

Here's an example:

Running this will print:

By running the create_ref function, we can dynamically update the value of $original_var, which exists outside of it. Notice that the function doesn't even need to know about $original_var; it works on any variable name provided, making it generic.

In this script:

  • We declare a variable original_var with the value "Hello, World!".
  • The create_ref function takes the name of a variable as an argument.
  • Inside the function, declare -n ref="$ref_name" creates a nameref ref that points to the variable named by $ref_name.
  • By setting ref="Hello from nameref!", we indirectly update original_var.
  • Finally, we print original_var to see the updated value.

Without the nameref, you could achieve the same thing with this eval (read: evil) trick:

This achieves the same result. The eval "$var_name="$new_value"" dynamically updates the $original_var variable through $var_name. However, eval can be risky for security, and the nameref approach looks much cleaner syntactically.

Managing multiple arrays

Namerefs shine when you need to manage multiple arrays dynamically. Consider a scenario where you have several datasets stored in different arrays, and you want to process them using a single function.

This returns:

Here:

  • We declare three arrays: dataset1, dataset2, and dataset3.
  • The sum_array function takes the name of an array as an argument.
  • Using declare -n arr="$array_name", we create a nameref arr that points to the specified array.
  • We then iterate over the elements of arr to calculate the sum.
  • Finally, we call sum_array for each dataset, and the function correctly processes each array based on the reference.

Without the nameref, you could again use the eval trick to achieve the same thing, but this time it looks even uglier:

This approach is more complex, less secure, and harder to read in general. But the above eval example was a bit contrived to make it look bad. You can achieve the same thing without eval or nameref in this particular case like this:

Here, instead of passing the name of the dataset arrays as strings, we pass the elements of the array to the function and add them. But I digress!

Associative arrays and nested references

Namerefs also work with associative arrays and can be used for more complex data structures.

It prints:

And voilà! We have a function that can dynamically update the values in an associative array. This technique is useful for changing environments or contexts (staging/production) in shell scripts.

In this example:

  • We declare an associative array user_info containing user details.
  • The update_info function takes the name of the associative array, the key to update, and the new value.
  • Using declare -n info="$info_name", we create a nameref info pointing to user_info.
  • We update the specified key in the array.
  • Finally, we echo the updated user information.

Doing this with eval isn't pretty. I'll leave that as an exercise for you if you like to torment yourself.

Implementing generic setter and getter functions

Building on the earlier examples, you can use namerefs to create generic setter and getter functions, making it easier to manage configuration variables or environment settings in scripts.

Here's an example:

To keep things simple, the env variable isn't a CLI argument. Based on whether env is set to staging or production, the script will print the relevant database values.

For staging, you'll see:

For production:

Oh, one extra thing: nameref was introduced in Bash 4.3, so you might run into problems if you're using an ancient version like the one shipped with macOS.

Discussion in the ATmosphere

Loading comments...