Dynamic variable names in Bash
Asked Answered
E

20

304

I am confused about a bash script.

I have the following code:

function grep_search() {
    magic_way_to_define_magic_variable_$1=`ls | tail -1`
    echo $magic_variable_$1
}

I want to be able to create a variable name containing the first argument of the command and bearing the value of e.g. the last line of ls.

So to illustrate what I want:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

So, how should I define/declare $magic_way_to_define_magic_variable_$1 and how should I call it within the script?

I have tried eval, ${...}, \$${...}, but I am still confused.

Einsteinium answered 14/5, 2013 at 21:22 Comment(4)
Don't. Use an associative array to map the command name to the data.Gob
VAR=A; VAL=333; read "$VAR" <<< "$VAL"; echo "A = $A"Maggiemaggio
When can this be useful?Showalter
@Showalter For example with argument indexes, as in "${!ARGUMENT_INDEX:-default}"Trejo
G
214

Use an associative array, with command names as keys.

# Requires bash 4, though
declare -A magic_variable=()

function grep_search() {
    magic_variable[$1]=$( ls | tail -1 )
    echo ${magic_variable[$1]}
}

If you can't use associative arrays (e.g., you must support bash 3), you can use declare to create dynamic variable names:

declare "magic_variable_$1=$(ls | tail -1)"

and use indirect parameter expansion to access the value.

var="magic_variable_$1"
echo "${!var}"

See BashFAQ: Indirection - Evaluating indirect/reference variables.

Gob answered 14/5, 2013 at 21:43 Comment(16)
@DeaDEnD -a declares an indexed array, not an associative array. Unless the argument to grep_search is a number, it will be treated as a parameter with a numeric value (which defaults to 0 if the parameter isn't set).Gob
Hmm. I'm using bash 4.2.45(2) and declare doesn't list it as an option declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. It seems to be working correctly however.Hebraize
declare -h in 4.2.45(2) for me shows declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. You might double-check that you are actually running 4.x and not 3.2.Gob
Why not just declare $varname="foo"?Anatomical
Does anyone know of a pure POSIX way of doing this, that would work with sh/dash?Wore
Is it possible to add an element to the magic_variable array without specifing an index?Shaneka
Not in an associative array. For an indexed array, the integer keys have an increment function available (in the mathematical sense): arr+=(foo) is short for arr[${#arr}]=foo, since the length of an array is one greater than the last index. There's no such function for arbitrary string keys used by an associative array.Gob
You can't trust that the index is sequential. ${#arr} is just the count, not the next available index. For that, add one to the last index in the array: for i in "${!arr[@]}"; do n=$((++i)); done or even for n in "${!a[@]}"; do :; done; ((n++))Sublittoral
can we export after calling declare?Oracle
@AlexanderMills Yes; export simply adds the export attribute to a name.Gob
${!varname} is much simpler and widely compatibleSubzero
I downvoted because it does not first answer the question before suggesting an alternative which make the real answer easy to miss so I moved on to find the answer in Maëlan's post. I only later read this answer and realized it did indeed include the answer, but buried at the end. Wishing more S.O. answers would first answer the question before suggesting an alternative that does not apply in all use-cases.Missive
@BradHein You can't use ${!varname} to set the variable. I'm also not sure what you mean by "widely compatible". It's not part of the POSIX standard, and zsh uses an entirely different syntax.Gob
@Missive POSIX doesn't provide a safe way to handle the original question, and using an associative array is much simpler than hacks involving declare (which itself is not entirely safe). At this point, the only people who need bash 3 solutions are those stubbornly insisting that the version that ships with macOS would be preferable to installing a modern version of bash or switching to zsh, an up-to-date version of which has been included with macOS for years. I don't mind a downvote for that reason, but I stand by my decision to de-emphasize the approach.Gob
The point is, first answer the question, then get on your soapbox as much as you want. So I stand by my downvoting for answers that de-emphasize informing over preaching. As for your assertion about the stubbornness of people, you are wrong. There are those who want their script to run on macOS w/o having the target user be forced to change their computer just to run a script. It's one thing for me to decide to change to zsh or upgrade bash, it's yet another for me to force it on users needing to do a small task. And it is ironic that you refer to these people as "stubborn."Missive
Using an associative array it's probably the way not to goGroundsheet
W
398

I've been looking for better way of doing it recently. Associative array sounded like overkill for me. Look what I found:

suffix=bzz
declare prefix_$suffix=mystr

...and then...

varname=prefix_$suffix
echo ${!varname}

From the docs:

The ‘$’ character introduces parameter expansion, command substitution, or arithmetic expansion. ...

The basic form of parameter expansion is ${parameter}. The value of parameter is substituted. ...

If the first character of parameter is an exclamation point (!), and parameter is not a nameref, it introduces a level of indirection. Bash uses the value formed by expanding the rest of parameter as the new parameter; this is then expanded and that value is used in the rest of the expansion, rather than the expansion of the original parameter. This is known as indirect expansion. The value is subject to tilde expansion, parameter expansion, command substitution, and arithmetic expansion. ...

Wagoner answered 8/8, 2013 at 11:0 Comment(16)
If want to declare a global inside a function, can use "declare -g" in bash >= 4.2. In earlier bash, can use "readonly" instead of "declare", so long as you don't want to change the value later. Can be okay for configuration or what have you.Hema
best to use encapsulated variable format: prefix_${middle}_postfix (ie. your formatting wouldn't not work for varname=$prefix_suffix)Conversation
I was stuck with bash 3 and could not use associative arrays; as such this was a life saver. ${!...} not easy to google on that one. I assume it just expands a var name.Stutter
@NeilMcGill: See "man bash" gnu.org/software/bash/manual/html_node/… : The basic form of parameter expansion is ${parameter}. <...> 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.Wagoner
Looks quite nice ... BUT ... what if we want to assign values to our dynamic variables? Numeric values for instance? I guess this is a little trickier even...Endarch
@syntaxerror: you can assign values as much as you want with "declare" command above.Wagoner
YES, declare works! Thank you. This took me a while to get that to work. MUST use $, though. To not just add one more pointless "thank you" post, here's my use case: (vars and values read from file; ${vars[0]} be x, ${values[0]} be 8). tmpvar=${vars[0]}; declare $tmpvar=${values[0]}; echo "${vars[0]} is ${!tmpvar}". --- The echo line will spit out x is 8 even though we never assigned it by hand.Endarch
When setting arrays, hashmaps and global variables, using declare to set the value has a drawback: you have to declare it with the correct flags (e.g., declare -gA $VAR[$K]=$V). But you could use eval instead. E.g., eval "$VAR['$K']='$V'".Rustic
@SamWatkins how do you know that declare -g is supported from bash 4.2? Do you know any official docs where it states about changes in each version, specifically on this -g flag for declare. I tried looking here but there was not version noteCheque
Nice - it even works in bash 3.2, which is version on macOS (now and probably forever, due to Apple policy).Shinberg
Forget declare just use ${!varname} syntax. ex. X=PATH; echo ${!X} prints the contents of the PATH variable.Subzero
For some reason, in Mac OS echo ${!varname} works but not in a script when sourced. Can it be made to work when sourced?Mcquiston
@Mcquiston Which shell are you sourcing it from? You can check with echo $SHELL. macOS has zsh as a default shell these days, and ${!varname} is specifiic for Bash.Wagoner
I'm using #!/usr/bin/env bash at the top of the script to use bash on Mac OS and Linux.Mcquiston
@Mcquiston Note that if you source the script, the shebang (#!) line is ignored. Is is parsed only when you run the script directly.Wagoner
Is there a way to get this to work in Zsh?Resorcinol
G
214

Use an associative array, with command names as keys.

# Requires bash 4, though
declare -A magic_variable=()

function grep_search() {
    magic_variable[$1]=$( ls | tail -1 )
    echo ${magic_variable[$1]}
}

If you can't use associative arrays (e.g., you must support bash 3), you can use declare to create dynamic variable names:

declare "magic_variable_$1=$(ls | tail -1)"

and use indirect parameter expansion to access the value.

var="magic_variable_$1"
echo "${!var}"

See BashFAQ: Indirection - Evaluating indirect/reference variables.

Gob answered 14/5, 2013 at 21:43 Comment(16)
@DeaDEnD -a declares an indexed array, not an associative array. Unless the argument to grep_search is a number, it will be treated as a parameter with a numeric value (which defaults to 0 if the parameter isn't set).Gob
Hmm. I'm using bash 4.2.45(2) and declare doesn't list it as an option declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. It seems to be working correctly however.Hebraize
declare -h in 4.2.45(2) for me shows declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. You might double-check that you are actually running 4.x and not 3.2.Gob
Why not just declare $varname="foo"?Anatomical
Does anyone know of a pure POSIX way of doing this, that would work with sh/dash?Wore
Is it possible to add an element to the magic_variable array without specifing an index?Shaneka
Not in an associative array. For an indexed array, the integer keys have an increment function available (in the mathematical sense): arr+=(foo) is short for arr[${#arr}]=foo, since the length of an array is one greater than the last index. There's no such function for arbitrary string keys used by an associative array.Gob
You can't trust that the index is sequential. ${#arr} is just the count, not the next available index. For that, add one to the last index in the array: for i in "${!arr[@]}"; do n=$((++i)); done or even for n in "${!a[@]}"; do :; done; ((n++))Sublittoral
can we export after calling declare?Oracle
@AlexanderMills Yes; export simply adds the export attribute to a name.Gob
${!varname} is much simpler and widely compatibleSubzero
I downvoted because it does not first answer the question before suggesting an alternative which make the real answer easy to miss so I moved on to find the answer in Maëlan's post. I only later read this answer and realized it did indeed include the answer, but buried at the end. Wishing more S.O. answers would first answer the question before suggesting an alternative that does not apply in all use-cases.Missive
@BradHein You can't use ${!varname} to set the variable. I'm also not sure what you mean by "widely compatible". It's not part of the POSIX standard, and zsh uses an entirely different syntax.Gob
@Missive POSIX doesn't provide a safe way to handle the original question, and using an associative array is much simpler than hacks involving declare (which itself is not entirely safe). At this point, the only people who need bash 3 solutions are those stubbornly insisting that the version that ships with macOS would be preferable to installing a modern version of bash or switching to zsh, an up-to-date version of which has been included with macOS for years. I don't mind a downvote for that reason, but I stand by my decision to de-emphasize the approach.Gob
The point is, first answer the question, then get on your soapbox as much as you want. So I stand by my downvoting for answers that de-emphasize informing over preaching. As for your assertion about the stubbornness of people, you are wrong. There are those who want their script to run on macOS w/o having the target user be forced to change their computer just to run a script. It's one thing for me to decide to change to zsh or upgrade bash, it's yet another for me to force it on users needing to do a small task. And it is ironic that you refer to these people as "stubborn."Missive
Using an associative array it's probably the way not to goGroundsheet
B
171

Beyond associative arrays, there are several ways of achieving dynamic variables in Bash. Note that all these techniques present risks, which are discussed at the end of this answer.

In the following examples I will assume that i=37 and that you want to alias the variable named var_37 whose initial value is lolilol.

Method 1. Using a “pointer” variable

You can simply store the name of the variable in an indirection variable, not unlike a C pointer. Bash then has a syntax for reading the aliased variable: ${!name} expands to the value of the variable whose name is the value of the variable name. You can think of it as a two-stage expansion: ${!name} expands to $var_37, which expands to lolilol.

name="var_$i"
echo "$name"         # outputs “var_37”
echo "${!name}"      # outputs “lolilol”
echo "${!name%lol}"  # outputs “loli”
# etc.

Unfortunately, there is no counterpart syntax for modifying the aliased variable. Instead, you can achieve assignment with one of the following tricks.

1a. Assigning with eval

eval is evil, but is also the simplest and most portable way of achieving our goal. You have to carefully escape the right-hand side of the assignment, as it will be evaluated twice. An easy and systematic way of doing this is to evaluate the right-hand side beforehand (or to use printf %q).

And you should check manually that the left-hand side is a valid variable name, or a name with index (what if it was evil_code # ?). By contrast, all other methods below enforce it automatically.

# check that name is a valid variable name:
# note: this code does not support variable_name[index]
shopt -s globasciiranges
[[ "$name" == [a-zA-Z_]*([a-zA-Z_0-9]) ]] || exit

value='babibab'
eval "$name"='$value'  # carefully escape the right-hand side!
echo "$var_37"  # outputs “babibab”

Downsides:

  • does not check the validity of the variable name.
  • eval is evil.
  • eval is evil.
  • eval is evil.

1b. Assigning with read

The read builtin lets you assign values to a variable of which you give the name, a fact which can be exploited in conjunction with here-strings:

IFS= read -r -d '' "$name" <<< 'babibab'
echo "$var_37"  # outputs “babibab\n”

The IFS part and the option -r make sure that the value is assigned as-is, while the option -d '' allows to assign multi-line values. Because of this last option, the command returns with an non-zero exit code.

Note that, since we are using a here-string, a newline character is appended to the value.

Downsides:

  • somewhat obscure;
  • returns with a non-zero exit code;
  • appends a newline to the value.

1c. Assigning with printf

Since Bash 3.1 (released 2005), the printf builtin can also assign its result to a variable whose name is given. By contrast with the previous solutions, it just works, no extra effort is needed to escape things, to prevent splitting and so on.

printf -v "$name" '%s' 'babibab'
echo "$var_37"  # outputs “babibab”

Downsides:

  • Less portable (but, well).

Method 2. Using a “reference” variable

Since Bash 4.3 (released 2014), the declare builtin has an option -n for creating a variable which is a “name reference” to another variable, much like C++ references. Just as in Method 1, the reference stores the name of the aliased variable, but each time the reference is accessed (either for reading or assigning), Bash automatically resolves the indirection.

In addition, Bash has a special and very confusing syntax for getting the value of the reference itself, judge by yourself: ${!ref}.

declare -n ref="var_$i"
echo "${!ref}"  # outputs “var_37”
echo "$ref"     # outputs “lolilol”
ref='babibab'
echo "$var_37"  # outputs “babibab”

This does not avoid the pitfalls explained below, but at least it makes the syntax straightforward.

Downsides:

  • Not portable.

Risks

All these aliasing techniques present several risks. The first one is executing arbitrary code each time you resolve the indirection (either for reading or for assigning). Indeed, instead of a scalar variable name, like var_37, you may as well alias an array subscript, like arr[42]. But Bash evaluates the contents of the square brackets each time it is needed, so aliasing arr[$(do_evil)] will have unexpected effects… As a consequence, only use these techniques when you control the provenance of the alias.

function guillemots {
  declare -n var="$1"
  var="«${var}»"
}

arr=( aaa bbb ccc )
guillemots 'arr[1]'  # modifies the second cell of the array, as expected
guillemots 'arr[$(date>>date.out)1]'  # writes twice into date.out
            # (once when expanding var, once when assigning to it)

The second risk is creating a cyclic alias. As Bash variables are identified by their name and not by their scope, you may inadvertently create an alias to itself (while thinking it would alias a variable from an enclosing scope). This may happen in particular when using common variable names (like var). As a consequence, only use these techniques when you control the name of the aliased variable.

function guillemots {
  # var is intended to be local to the function,
  # aliasing a variable which comes from outside
  declare -n var="$1"
  var="«${var}»"
}

var='lolilol'
guillemots var  # Bash warnings: “var: circular name reference”
echo "$var"     # outputs anything!

Source:

Bal answered 25/3, 2019 at 3:54 Comment(8)
This is the best answer, particularly since the ${!varname} technique requires an intermediate var for varname.Shinberg
Hard to understand that this answer has not been upvoted higherEmigrant
The only qualm I have with this answer is its use of the gratuitously incompatible function funcname() { syntax; it's spot-on on everything that's actually pertinent to the question. :)Ked
@Bal - You say: "All these aliasing techniques present several risks." What risks does printf -v present? (Other than not being portable to versions of bash that are over 17 years old.)Rape
@Rape the risk shown in the sentence right after that one. :-) if name='x[$(evil)]' then each printf -v "$name" '%s' '...' evaluates evil.Indophenol
@CharlesDuffy I admit I preferred that syntax at some point, shame on me! Now fixed.Indophenol
@Bal @CharlesDuffy I have a pretty handy convention. For general/public functions: function do_stuff { :; } For internal/private functions _get_real_script_dir(){ :; } that way I can <tools.sh grep 'function' to get a list of functions when I'm searching for something I know I've got an example of somewhere. I didn't know that combining function and () was obsolete. I have a LOT of code to update. Thanks for letting me know to stop perpetuating this in my teams.Leonidaleonidas
May I ask how the printf %q work in referring to a dynamic variable name?Cher
M
26

Example below returns value of $name_of_var

var=name_of_var
echo $(eval echo "\$$var")
Microcircuit answered 17/10, 2017 at 13:45 Comment(1)
Nesting two echos with a command substitution (which misses quotes) is unnecessary. Plus, option -n should be given to echo. And, as always, eval is unsafe. But all of this is unnecessary since Bash has a safer, clearer and shorter syntax for this very purpose: ${!var}.Indophenol
G
17

Use declare

There is no need on using prefixes like on other answers, neither arrays. Use just declare, double quotes, and parameter expansion.

I often use the following trick to parse argument lists contanining one to n arguments formatted as key=value otherkey=othervalue etc=etc, Like:

# brace expansion just to exemplify
for variable in {one=foo,two=bar,ninja=tip}
do
  declare "${variable%=*}=${variable#*=}"
done
echo $one $two $ninja 
# foo bar tip

But expanding the argv list like

for v in "$@"; do declare "${v%=*}=${v#*=}"; done

Extra tips

# parse argv's leading key=value parameters
for v in "$@"; do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
done
# consume argv's leading key=value parameters
while test $# -gt 0; do
  case "$1" in ?*=?*) declare "${1%=*}=${1#*=}";; *) break;; esac
  shift
done
Groundsheet answered 12/10, 2019 at 5:9 Comment(4)
This looks like a very clean solution. No evil bibs and bobs and you use tools which are related to variables, not obscure seemingly unrelated or even dangerous functions such as printf or evalCurtiscurtiss
To prevent squirrely behavior on nutty input (e.g. key=oops=val), you should really be using the "longest matching pattern" form of these string-trimming expansions: "${v%%=*}=${v##*=}". Applies to every occurrence in your answer.Asquith
Or if you can tolerate = symbols in values, perhaps only use the "longest matching suffix" expansion and always add quotes around your values: "${v%%=*}='${v#*=}'". This would handle key=oops=val as key='oops=val', that is, declare key with value oops=val.Asquith
You are both right, but may think if having = within keys or values makes sense for such a simple argv parsing strategyGroundsheet
I
14

Combining two highly rated answers here into a complete example that is hopefully useful and self-explanatory:

#!/bin/bash

intro="You know what,"
pet1="cat"
pet2="chicken"
pet3="cow"
pet4="dog"
pet5="pig"

# Setting and reading dynamic variables
for i in {1..5}; do
        pet="pet$i"
        declare "sentence$i=$intro I have a pet ${!pet} at home"
done

# Just reading dynamic variables
for i in {1..5}; do
        sentence="sentence$i"
        echo "${!sentence}"
done

echo
echo "Again, but reading regular variables:"
echo $sentence1
echo $sentence2
echo $sentence3
echo $sentence4
echo $sentence5

Output:

You know what, I have a pet cat at home
You know what, I have a pet chicken at home
You know what, I have a pet cow at home
You know what, I have a pet dog at home
You know what, I have a pet pig at home

Again, but reading regular variables:
You know what, I have a pet cat at home
You know what, I have a pet chicken at home
You know what, I have a pet cow at home
You know what, I have a pet dog at home
You know what, I have a pet pig at home

Inspan answered 26/11, 2020 at 11:31 Comment(0)
H
7

This will work too

my_country_code="green"
x="country"

eval z='$'my_"$x"_code
echo $z                 ## o/p: green

In your case

eval final_val='$'magic_way_to_define_magic_variable_"$1"
echo $final_val
Heteroplasty answered 19/10, 2018 at 9:38 Comment(0)
M
6

For zsh (newers mac os versions), you should use

real_var="holaaaa"
aux_var="real_var"
echo ${(P)aux_var}
holaaaa

Instead of "!"

Marking answered 25/12, 2020 at 16:38 Comment(3)
What does the P mean?Resorcinol
It's explained in man zshall, section PARAMETER EXPANSION, subsection Parameter Expansion Flags: P: This forces the value of the parameter name to be interpreted as a further parameter name, whose value will be used where appropriate. [...]Offend
You are amazing, this worked to test locally and then added "!" on the other configuration. Thank you!!!Breeching
A
4

This should work:

function grep_search() {
    declare magic_variable_$1="$(ls | tail -1)"
    echo "$(tmpvar=magic_variable_$1 && echo ${!tmpvar})"
}
grep_search var  # calling grep_search with argument "var"
Abb answered 12/7, 2015 at 13:52 Comment(0)
L
4

As per BashFAQ/006, you can use read with here string syntax for assigning indirect variables:

function grep_search() {
  read "$1" <<<$(ls | tail -1);
}

Usage:

$ grep_search open_box
$ echo $open_box
stack-overflow.txt
Lagos answered 14/4, 2018 at 1:4 Comment(0)
S
4

An extra method that doesn't rely on which shell/bash version you have is by using envsubst. For example:

newvar=$(echo '$magic_variable_'"${dynamic_part}" | envsubst)
Stowe answered 28/11, 2019 at 19:47 Comment(1)
thx for the one line version. The only condition, that the variable should be exported, otherwise envsubst will not see it.Girardo
H
3

Even though it's an old question, I still had some hard time with fetching dynamic variables names, while avoiding the eval (evil) command.

Solved it with declare -n which creates a reference to a dynamic value, this is especially useful in CI/CD processes, where the required secret names of the CI/CD service are not known until runtime. Here's how:

# Bash v4.3+
# -----------------------------------------------------------
# Secerts in CI/CD service, injected as environment variables
# AWS_ACCESS_KEY_ID_DEV, AWS_SECRET_ACCESS_KEY_DEV
# AWS_ACCESS_KEY_ID_STG, AWS_SECRET_ACCESS_KEY_STG
# -----------------------------------------------------------
# Environment variables injected by CI/CD service
# BRANCH_NAME="DEV"
# -----------------------------------------------------------
declare -n _AWS_ACCESS_KEY_ID_REF=AWS_ACCESS_KEY_ID_${BRANCH_NAME}
declare -n _AWS_SECRET_ACCESS_KEY_REF=AWS_SECRET_ACCESS_KEY_${BRANCH_NAME}

export AWS_ACCESS_KEY_ID=${_AWS_ACCESS_KEY_ID_REF}
export AWS_SECRET_ACCESS_KEY=${_AWS_SECRET_ACCESS_KEY_REF}

echo $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY
aws s3 ls
Homan answered 29/8, 2020 at 13:1 Comment(0)
I
3

KISS approach:

a=1
c="bam"
let "$c$a"=4
echo $bam1

results in 4

Inexecution answered 12/6, 2021 at 2:39 Comment(5)
"echo bam1" will output "bam1", not "4"Transmigrant
How is this related to my response? You're echoing a string, because you're missing the $.Inexecution
You were missing the $ in your answer. I commented. Later, someone edit/corrected your answer.Transmigrant
ahh, OK.... now all 4 comments make no sense anymore.Inexecution
let only handles arithmetic, does not solve OP's problemClassicist
H
2

Wow, most of the syntax is horrible! Here is one solution with some simpler syntax if you need to indirectly reference arrays:

#!/bin/bash

foo_1=(fff ddd) ;
foo_2=(ggg ccc) ;

for i in 1 2 ;
do
    eval mine=( \${foo_$i[@]} ) ;
    echo ${mine[@]}" " ;
done ;

For simpler use cases I recommend the syntax described in the Advanced Bash-Scripting Guide.

Hampshire answered 5/1, 2017 at 2:14 Comment(5)
The ABS is someone notorious for showcasing bad practices in its examples. Please consider leaning on the bash-hackers wiki or the Wooledge wiki -- which has the directly on-topic entry BashFAQ #6 -- instead.Ked
This works only if the entries in foo_1 and foo_2 are free of whitespace and special symbols. Examples for problematic entries: 'a b' will create two entries inside mine. '' won't create an entry inside mine. '*' will expand to the content of the working directory. You can prevent these problems by quoting: eval 'mine=( "${foo_'"$i"'[@]}" )'Mlawsky
@Mlawsky That's a general problem with looping through any array in BASH. This could also be solved by temporarily changing the IFS (and then of course changing it back). It's good to see the quoting worked out.Hampshire
@Hampshire I beg to differ. It is not a general problem. There is a standard solution: Always quote [@] constructs. "${array[@]}" will always expand to the correct list of entries without problems like word splitting or expansion of *. Also, the word splitting problem can only be circumvented with IFS if you know any non-null character that does never appear inside the array. Furthermore literal treatment of * cannot be achieved by setting IFS. Either you set IFS='*' and split at the stars or you set IFS=somethingOther and the * expands.Mlawsky
@Mlawsky The general problem in loops is that tokenization occurs by default so that quoting is the special solution to allow extended strings that contain tokens. I updated the answer to remove the quoted array values which confused readers. The point of this answer was to create a simpler syntax, not a specific answer for a use case where quotes are needed to detail extended variables. Assignment quoting for specific use cases can be left to other developers' imagination.Hampshire
L
1

I want to be able to create a variable name containing the first argument of the command

script.sh file:

#!/usr/bin/env bash
function grep_search() {
  eval $1=$(ls | tail -1)
}

Test:

$ source script.sh
$ grep_search open_box
$ echo $open_box
script.sh

As per help eval:

Execute arguments as a shell command.


You may also use Bash ${!var} indirect expansion, as already mentioned, however it doesn't support retrieving of array indices.


For further read or examples, check BashFAQ/006 about Indirection.

We are not aware of any trick that can duplicate that functionality in POSIX or Bourne shells without eval, which can be difficult to do securely. So, consider this a use at your own risk hack.

However, you should re-consider using indirection as per the following notes.

Normally, in bash scripting, you won't need indirect references at all. Generally, people look at this for a solution when they don't understand or know about Bash Arrays or haven't fully considered other Bash features such as functions.

Putting variable names or any other bash syntax inside parameters is frequently done incorrectly and in inappropriate situations to solve problems that have better solutions. It violates the separation between code and data, and as such puts you on a slippery slope toward bugs and security issues. Indirection can make your code less transparent and harder to follow.

Lagos answered 14/4, 2018 at 0:50 Comment(0)
P
1

While I think declare -n is still the best way to do it there is another way nobody mentioned it, very useful in CI/CD

function dynamic(){
  export a_$1="bla"
}

dynamic 2
echo $a_2

This function will not support spaces so dynamic "2 3" will return an error.

Pasqualepasqueflower answered 9/2, 2022 at 17:57 Comment(1)
You can make this particular example safe with spaces by removing them: export a_${1//[^[:word:]]}="bla" with "2 3" will export a_23="bla". But since [:word:] includes numerals, which are illegal as the first symbol in a var identifier, you have to either include a legal prefix (as in your example), or be more aggressive in what you remove: export ${1//[^[:alpha:]_]}="bla" will strip everything except letters and underscores ("2 3" will export ="bla" – a syntax error :)).Asquith
C
0

For indexed arrays, you can reference them like so:

foo=(a b c)
bar=(d e f)

for arr_var in 'foo' 'bar'; do
    declare -a 'arr=("${'"$arr_var"'[@]}")'
    # do something with $arr
    echo "\$$arr_var contains:"
    for char in "${arr[@]}"; do
        echo "$char"
    done
done

Associative arrays can be referenced similarly but need the -A switch on declare instead of -a.

Chabot answered 20/7, 2017 at 4:30 Comment(0)
M
0

POSIX compliant answer

For this solution you'll need to have r/w permissions to the /tmp folder.
We create a temporary file holding our variables and leverage the -a flag of the set built-in:

$ man set
...
-a Each variable or function that is created or modified is given the export attribute and marked for export to the environment of subsequent commands.

Therefore, if we create a file holding our dynamic variables, we can use set to bring them to life inside our script.

The implementation

#!/bin/sh
# Give the temp file a unique name so you don't mess with any other files in there
ENV_FILE="/tmp/$(date +%s)"

MY_KEY=foo
MY_VALUE=bar

echo "$MY_KEY=$MY_VALUE" >> "$ENV_FILE"

# Now that our env file is created and populated, we can use "set"
set -a; . "$ENV_FILE"; set +a
rm "$ENV_FILE"
echo "$foo"

# Output is "bar" (without quotes)

Explaining the steps above:

# Enables the -a behavior
set -a

# Sources the env file
. "$ENV_FILE"

# Disables the -a behavior
set +a
Monogamous answered 23/1, 2022 at 0:2 Comment(0)
J
0

Careful: I just found this works very differently - doesn't work - in zsh. So make sure you're in the shell you mean to be in; or will be in for your script's eventual execution context.

Jaynes answered 17/5, 2023 at 18:14 Comment(1)
This is tagged as bash so if you use one of the answers here which is not mentioned for zsh and use it on zsh then it's on you.Wagshul
D
-3

for varname=$prefix_suffix format, just use:

varname=${prefix}_suffix
Doyenne answered 13/1, 2015 at 9:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.