Bash Indirect Variable Reference

Bash indirect variable reference

This is really quite awkward in bash: you require a temporary variable to hold the constructed variable name:

$ var_1=hello
$ num=1
$ tmp=var_$num
$ echo ${!tmp}
hello

Arrays are much simpler

$ var=( [1]=world )
$ echo ${var[$num]}
world

Bash indirect variable referencing

Use bash indirect variable reference:

${!var}

And of course can be done with eval, not recommended:

eval 'echo $'"$var"

Why:

$ bar=xyz

$ var='bar;whoami'

$ eval 'echo $'"$var"
xyz
spamegg

Th command whoami is being evaluated too as part of evaluation by eval, imagine a destructive command instead of whoami.


Example:

$ bar='xyz'

$ var=bar

$ echo "${!var}"
xyz

$ eval 'echo $'"$var"
xyz

What is indirect expansion? What does ${!var*} mean?

If you read the bash man page, it basically confirms what you have stated:

If the first character of parameter is an exclamation point (!), a level of variable indirection is introduced. Bash uses the value of the variable formed from the rest of parameter as the name of the variable; this variable is then expanded and that value is used in the rest of the substitution, rather than the value of parameter itself. This is known as indirect expansion.

However, reading on from there:

The exceptions to this are the expansions of ${!prefix*} and ${!name[@]} described below.

${!prefix*} Names matching prefix. Expands to the names of variables whose names begin with prefix, separated by the first character of the IFS special variable.

In other words, your particular example ${!N*} is an exception to the rule you quoted. It does, however, work as advertised in the expected cases, such as:

$ export xyzzy=plugh ; export plugh=cave

$ echo ${xyzzy} # normal, xyzzy to plugh
plugh

$ echo ${!xyzzy} # indirection, xyzzy to plugh to cave
cave

Using indirect variable references in a Bash script

It seems like you're looking for an indirect variable reference. Your $cmd variable points to the variable $Apr and you want to use the value of $Apr.

Here's how you can do this (just the relevant lines):

cmd=$(date | awk '{print $2}')
eval month=\$$cmd
dat=$(ls -ltr | grep $month | awk '{print $9}')

The eval command resolves the indirect reference created by \$$cmd.

More details here: http://www.tldp.org/LDP/abs/html/ivr.html

Indirect variable assignment in bash

The main point is that the recommended way to do this is:

eval "$var=\$val"

with the RHS done indirectly too. Since eval is used in the same
environment, it will have $val bound, so deferring it works, and since
now it's just a variable. Since the $val variable has a known name,
there are no issues with quoting, and it could have even been written as:

eval $var=\$val

But since it's better to always add quotes, the former is better, or
even this:

eval "$var=\"\$val\""

A better alternative in bash that was mentioned for the whole thing that
avoids eval completely (and is not as subtle as declare etc):

printf -v "$var" "%s" "$val"

Though this is not a direct answer what I originally asked...

Bash indirect reference where one var references multiple

eval sound quite scary...

here is a better solution:

Bash Templating: How to build configuration files from templates with Bash?

you just have to tweak it a bit so it follows your syntax.

How to iterate over an array using indirect reference?

${!ARRAYNAME[@]} means "the indices of ARRAYNAME". As stated in the bash man page since ARRAYNAME is set, but as a string, not an array, it returns 0.

Here's a solution using eval.

#!/usr/bin/env bash

ARRAYNAME='FRUITS'
FRUITS=( APPLE BANANA ORANGE )

eval array=\( \${${ARRAYNAME}[@]} \)

for fruit in "${array[@]}"; do
echo ${fruit}
done

What you were originally trying to do was create an Indirect Reference. These were introduced in bash version 2 and were meant to largely replace the need for eval when trying to achieve reflection-like behavior in the shell.

What you have to do when using indirect references with arrays is include the [@] in your guess at the variable name:

#!/usr/bin/env bash

ARRAYNAME='FRUITS'
FRUITS=( APPLE BANANA ORANGE )

array="${ARRAYNAME}[@]"
for fruit in "${!array}"; do
echo $fruit
done

All that said, it's one thing to use Indirect References in this trivial example, but, as indicated in the link provided by Dennis Williamson, you should be hesitant to use them in real-world scripts. They are all but guaranteed to make your code more confusing than necessary. Usually you can get the functionality you need with an Associative Array.

Declaring variables with indirect reference with a prefix

If your Bash is recent enough, then use the -n indirect variable attribute like this:

#!/usr/bin/env bash

source values.sh
read -r -p 'Enter Identifier : ' pk

declare -n \
value1="${pk}_value1" \
value2="${pk}_value2" \
value3="${pk}_value3"

Alternate method with populating an associative array from the file values.sh:

#!/usr/bin/env bash

declare -A values="($(
xargs -l1 \
bash -c \
'IFS="=" read -r k v <<<"$@"; printf "[%q]=%q\n" "$k" "$v"' _ \
<values.sh
))"

read -r -p 'Enter Identifier : ' pk

declare -- \
value1="${values[${pk}_value1]}" \
value2="${values[${pk}_value2]}" \
value3="${values[${pk}_value3]}"

Working of the Associative array population:

xargs -l1 will translate the stdio input stream's lines (here: <values.sh) into arguments to a command.

The command called by xargs is bash -c, witch executes an inline script detailed here:

# Read variables k and v from the arguments
# streamed as a here-string <<<"",
# using the = sign as the Internal Field Separator.
# Actually splitting key=value into k and v.
IFS="=" read -r k v <<<"$@"

# Format variables k and v into an Associative array
# entry declaration in the form [key]=value,
# with %q adding quotes or escaping if required.
printf "[%q]=%q\n" "$k" "$v"

Finally the Associative array declaration and assignment declare -A values="($(commands))" gets the entries generated by the xarg and inline shell script commands.

bash indirect reference by reference

I think that would be unsafe. If $1 gets a value that's not dev or stg it would cause syntax error and other unexpected things may happen. Even with eval it wouldn't be a good idea unless you add a conditional filter. You could use a case statement:

case "$1" in
dev)
"$dev_path"/bin/execute
;;
std)
"$std_path"/bin/execute
;;
*)
echo "Invalid argument: $1"
;;
esac

Or make sure it's valid and use eval:

[[ $1 == dev || $1 == std ]] && eval "\"\${${1}_path}/bin/execute\""

Using if ... elif ... could be valid too but the case method is simpler.

As for variable indirection you would need to store "${1}_path" to another variable first which is unnecessary.

You could also use dev|std) eval ... ;; in the case statement at your preference.



Related Topics



Leave a reply



Submit