I am trying to pass a "var name" to a function, have the function transform the value the variable with such "var name" contains and then be able to reference the transformed object by its original "var name".
For example, let's say I have a function that converts a delimited list into an array and I have a delimited list named 'animal_list'. I want to convert that list to an array by passing the list name into the function and then reference, the now array, as 'animal_list'.
Code Example:
function delim_to_array() {
local list=$1
local delim=$2
local oifs=$IFS;
IFS="$delim";
temp_array=($list);
IFS=$oifs;
# Now I have the list converted to an array but it's
# named temp_array. I want to reference it by its
# original name.
}
# ----------------------------------------------------
animal_list="anaconda, bison, cougar, dingo"
delim_to_array ${animal_list} ","
# After this point I want to be able to deal with animal_name as an array.
for animal in "${animal_list[@]}"; do
echo "NAME: $animal"
done
# And reuse this in several places to converted lists to arrays
people_list="alvin|baron|caleb|doug"
delim_to_array ${people_list} "|"
# Now I want to treat animal_name as an array
for person in "${people_list[@]}"; do
echo "NAME: $person"
done
Best Answer
Description
Understanding this will take some effort. Be patient. The solution will work correctly in bash. Some "bashims" are needed.
First: We need to use the "Indirect" access to a variable
${!variable}
. If$variable
contains the stringanimal_name
, the "Parameter Expansion":${!variable}
will expand to the contents of$animal_name
.Lets see that idea in action, I have retained the names and values you used where possible to make it easier for you to understand:
If that complete script is executed (Let's assume its named so-setvar.sh), you should see:
Understand that "inside" means "inside the function", and "outside" the opposite.
The value inside
$VarName
is the name of the var:animal_list
, as a string.The value of
${!VarName}
is show to be the list:anaconda, bison, cougar, dingo
Now, to show how the solution is constructed, there is a line with echo:
which shows what the following line with
eval
executes:Once that is evaluated, the variable
a
is an array with the animal list. In this instance, the var a is used to show exactly how the eval affects it.And then, the values of each element of
a
are printed as<in> val
.And the same is executed in the outside part of the function as
<out> val
That is shown in this two lines:
Note that the real change was executed in the last eval of the function.
That's it, done. The var now has an array of values.
In fact, the core of the function is one line:
eval $VarName\=\(${!VarName}\)
Also, the value of IFS is set as local to the function which makes it return to the value it had before executing the function without any additional work. Thanks to Peter Cordes for the comment on the original idea.
That ends the explanation, hope its clear.
Real Function
If we remove all the unneeded lines to leave only the core eval, only create a new variable for IFS, we reduce the function to its minimal expression:
Setting the value of IFS as a local variable, allows us to also set a "default" value for the function. Whenever the value needed for IFS is not sent to the function as the second argument, the local IFS takes the "default" value. I felt that the default should be space ( ) (which is always an useful splitting value), the colon (:), and the vertical line (|). Any of those three will split the values. Of course, the default could be set to any other values that fit your needs.
Edit to use
read
:To reduce the risk of unquoted values in eval, we can use:
Most of the values set above for the var
animal_list
do fail with eval.But pass the read without problems.
eval
.Example
To really understand what, and how this function works, I re-wrote the code you posted using this function:
As you can see, the IFS is set only inside the function, it is not changed permanently, and therefore it does not need to be re-set to its old value. Additionally, the second call "people_list" to the function takes advantage of the default value of IFS, there is no need to set a second argument.
« Here be Dragons » ¯\_(ツ)_/¯
Warnings 01:
As the (eval) function was constructed, there is one place in which the var is exposed unquoted to the shell parsing. That allows us to get the "word splitting" done using the IFS value. But that also expose the values of the vars (unless some quoting prevent that) to: "brace expansion", "tilde expansion", "parameter, variable and arithmetic expansion", "command substitution", and "pathname expansion", In that order. And process substitution
<() >()
in systems that support it.An example of each (except last) is contained in this simple echo (be careful):
That is, any string that starts with
{~$`<>
or could match a file name, or contains?*[]
is a potential problem.If you are sure that the variables do not contain such problematic values, then you are safe. If there is the potential to have such values, the ways to answer your question are more complex and need more (even longer) descriptions and explanations. Using
read
is an alternative.Warnings 02:
Yes,
read
comes with it's own share of «dragons».read
command could get only one line. Multi-lines, even by setting the-d
option, need special care. Or the whole input will be assigned to one variable.IFS
value contains an space, leading and trailing spaces will be removed. Well, the complete description should include some detail about thetab
, but I'll skip it.|
data to read. If you do, read will be in a sub-shell. All variables set in a sub-shell do not persist upon returning to the parent shell. Well, there are some workarounds, but, again, I'll skip the detail.I didn't mean to include the warnings and problems of read, but by popular request, I had to include them, sorry.