Bash: Indirectly referencing variables
October 11, 2021•329 words
Sometimes it comes in handy to modify a variable in a function by name.
Usually the first tool people reach for in this case eval
:
# sets a variable to a new value
setvar() {
eval $(printf "%s=%q" "$1" "$2")
}
setvar x 2
printf "x=%s\n" "$x" # prints x=2
This works but making sure that everything is quoted correctly is cumbersome.
Bash supports a safer way for this: namerefs
To use a nameref you must declare a variable as a nameref using the -n
argument.
After that, any changes made to the nameref are actually applied to the variable it is referring to.
Here's setvar
written using namerefs:
setvar() {
local -n varname="$1"
varname="$2"
}
setvar x 3
printf "x=%s\n" # prints x=3
# it also supports arrays:
declare -a x=()
setvar 'x[0]' 3
setvar 'x[1]' hello
setvar 'x[2]' world
declare -p x
# prints: declare -a x=([0]="3" [1]="hello" [2]="world")
Adding directories to PATH-like variables
Besides PATH
, there are other colon-separated variables that are used by various programs for lookup purposes, such as man
's MANPATH
or Ruby's RUBYLIB
.
Often these are modified in your ~/.bashrc
like this:
export PATH="$HOME/.yarn/bin:$PATH"
That works until you source your ~/.bashrc
again, for example because you edited it: you'll end up with the same entry twice on your PATH
.
Let's use namerefs to set PATH-like variables idempotently: calling add_to_path
with the same path twice will not change the resulting path.
add_to_path() {
local -n pathvar="$1"
local dir="$2"
# set IFS to : to split by colons
local IFS=:
local -a pathparts=($pathvar)
for part in "${pathparts[@]}"; do
if [[ "$part" == "$dir" ]]; then
return
fi
done
pathvar="$dir:$pathvar"
}
Trying it out in the shell, we can see that it works as expected:
$ P=a:b
$ add_to_path P first
$ [[ "$P" == "first:a:b" ]] && printf "OK\n"
OK
$ add_to_path P first
$ [[ "$P" == "first:a:b" ]] && printf "OK\n"
OK