Bash array expansion using variable indirection expansion - bash

I am trying to expand all values in an array I get to through indirect expansion:
> my_array=(coconut banana)
> echo "${my_array[#]}"
coconut banana
> my_array_name=my_array
> echo ${!my_array_name}
coconut
> echo "${!my_array_name[#]}"
0
I am erroneously using "List of array keys" in the last command because I don't know how to type the right command?
I would like to get:
coconut banana
possibly without resorting to some ugly eval hack.. Example of one suck hack:
> echo \${$my_array_name[#]}
${my_array[#]}
> eval echo \${$my_array_name[#]}
coconut banana
Note
my_array may contain values with spaces!
EDIT
In the function I am writing, my_array_name is set through "$1" so I cannot use that literally.
Similar to: https://unix.stackexchange.com/questions/20171/indirect-return-of-all-elements-in-an-array but I need to avoid using eval to protect from the nasty effects the script would have if the environment was "hacked" just at the right time...

This should work
my_array_name='my_array[#]'
echo "${!my_array_name}"
After comment : you have to create a string with the name of the array and '[#]', another example
my_array_name="$1"'[#]'
echo "${!my_array_name}"
After comment : test in a function
display_elem() {
local arrname
arrname="$1[#]"
printf "%s\n" "${!arrname}"
}
display_elem my_array

The problem is my_array_name=my_array. You need to retrieve all values of my_array. Try this instead:
my_array_name=${my_array[#]}
echo "${my_array_name[#]}"

Related

Bash printing value of array reference

p=0
array="host_01"
HostProjects[$p]="project_01"
AvgCr=1569.22
eval "${array}_${HostProjects[$p]}=$AvgCr"
echo "Host Credit is ${host_01_project_01}"
Gives me 1569.22
But how can I get the result 1569.22 from:
printf '%s\n' "${array}_${HostProjects[$p]}"
or even from:
echo "${array}_${HostProjects[$p]}"
Which gives me host_01_project_01
I have tried several things but it ends up in syntax errors.
Thanks.
If you're using Bash, indirect parameter expansion is your friend:
varname="${array}_${HostProjects[$p]}"
echo "The value you want is ${!varname}"

Evaluate variable in if statement

So I have an array like:
al_ap_version=('ap_version' '[[ $data -ne $version ]]')
And the condition gets evaluated inside a loop like:
for alert in alert_list; do
data=$(tail -1 somefile)
condition=$(eval echo \${$alert[1]})
if eval "$condition" ; then
echo SomeAlert
fi
done
Whilst this generally works with many scenarios, if $data returns something like "-/-" or "4.2.9", I get errors as it doesn't seem to like complex strings in the variable.
Obviously I can't enclose the variable in single quotes as it won't expand so I'm after any ideas to expand the $data variable (or indeed the $version var which suffers the same possible fate) in a way that the evaluation can handle?
Ignoring the fact that eval is probably super dangerous to use here (unless the data in somefile is controlled by you and only you), there are a few issues to fix in your example code.
In your for loop, alert_list needs to be $alert_list.
Also, as pointed out by #choroba, you should be using != instead of -ne since your input isn't always an integer.
Finally, while debugging, you can add set -x to the top of your script, or add -x to the end of your shebang line to enable verbose output (helps to determine how bash is expanding your variables).
This works for me:
#!/bin/bash -x
data=2.2
version=1
al_ap_version=('ap_version' '[[ $data != $version ]]')
alert_list='al_ap_version'
for alert in $alert_list; do
condition=$(eval echo \${$alert[1]})
if eval "$condition"; then
echo "alert"
fi
done
You could try a more functional approach, even though bash is only just barely capable of such things. On the whole, it is usually a lot easier to pack an action to be executed into a bash function and refer to it with the name of the function, than to try to maintain the action as a string to be evaluated.
But first, the use of an array of names of arrays is awkward. Let's get rid of it.
It's not clear to me the point of element 0, ap_version, in the array al_ap_version but I suppose it has something to do with error messages. If the order of alert processing isn't important, you could replace the list of names of arrays with a single associative array:
declare -A alert_list
alert_list[ap_version]=... # see below
alert_list[os_dsk]=...
and then process them with:
for alert_name in ${!alert_list[#]}; do
alert=${alert_list[$alert_name]}
...
done
Having done that, we can get rid of the eval, with its consequent ugly necessity for juggling quotes, by creating a bash function for each alert:
check_ap_version() {
(($version != $1))
}
Edit: It seems that $1 is not necessarily numeric, so it would be better to use a non-numeric comparison, although exact version match might not be what you're after either. So perhaps it would be better to use:
check_ap_version() {
[[ $version != $1 ]]
}
Note the convention that the first argument of the function is the data value.
Now we can insert the name of the function into the alert array, and call it indirectly in the loop:
declare -A alert_list
alert_list[ap_version]=check_ap_version
alert_list[os_dsk]=check_op_dsk
check_alerts() {
local alert_name alert
local data=$(tail -1 somefile)
for alert_name in ${!alert_list[#]}; do
alert=${alert_list[$alert_name]}
if $alert "$data"; then
signal_alert $alert_name
fi
done
}
If you're prepared to be more disciplined about the function names, you can avoid the associative array, and thereby process the alerts in order. Suppose, for example, that every function has the name check_<alert_name>. Then the above could be:
alert_list=(ap_version os_dsk)
check_alerts() {
local alert_name
local data=$(tail -1 somefile)
for alert_name in $alert_list[#]; do
if check_$alert_name "$data"; then
signal_alert $alert_name
fi
done
}

Iterating over variable name in bash script

I needed to run a script over a bunch of files, which paths were assigned to train1, train2, ... , train20, and I thought 'why not make it automatic with a bash script?'.
So I did something like:
train1=path/to/first/file
train2=path/to/second/file
...
train20=path/to/third/file
for i in {1..20}
do
python something.py train$i
done
which didn't work because train$i echoes train1's name, but not its value.
So I tried unsuccessfully things like $(train$i) or ${train$i} or ${!train$i}.
Does anyone know how to catch the correct value of these variables?
Use an array.
Bash does have variable indirection, so you can say
for varname in train{1..20}
do
python something.py "${!varname}"
done
The ! introduces the indirection, so "get the value of the variable named by the value of varname"
But use an array. You can make the definition very readable:
trains=(
path/to/first/file
path/to/second/file
...
path/to/third/file
)
Note that this array's first index is at position zero, so:
for ((i=0; i<${#trains[#]}; i++)); do
echo "train $i is ${trains[$i]}"
done
or
for idx in "${!trains[#]}"; do
echo "train $idx is ${trains[$idx]}"
done
You can use array:
train[1]=path/to/first/file
train[2]=path/to/second/file
...
train[20]=path/to/third/file
for i in {1..20}
do
python something.py ${train[$i]}
done
Or eval, but it awfull way:
train1=path/to/first/file
train2=path/to/second/file
...
train20=path/to/third/file
for i in {1..20}
do
eval "python something.py $train$i"
done

Properly Specify an Array and Element through Variables in a Shell Script

Consider the following nonsense array:
# KIND[ID]=NAME
MONKEYS[1]="Oo Oo"
MONKEYS[2]="Aa Aa"
MONKEYS[3]="Ba Nana"
LIONS[5]="Mister Mufasa"
LIONS[7]="Cocoa Puff"
LIONS[8]="Lala Leo"
TIGERS[13]="Ben Gal"
TIGERS[15]="Tee Eye Double Guh Err"
TIGERS[22]="Oh Esex Diez Punto Cuatro"
With a given KIND and ID, I'm attempting to build a string that resembles $NAME[$ID] to get the associated name.
When explicitly stating an array name, the command behaves as expected echo "${LIONS[5]}"=>"Mister Mufasa"). However, whenever a variable is used, the shell responds with the given character in the string.
$LIONS[5] => 'e' # The fifth letter in "Mister Mufasa"
In other cases, I can't find a way to control interpolation to get the NAME
KIND="LIONS"
ID="5"
# Attempt to return value of `LIONS` when `KIND=LIONS`
echo $"${KIND}"; echo "\$${KIND}" #=> "$LIONS"
echo "$${KIND}" #=> "57800{KIND}" Interpolates "$$"
echo "\$\${KIND}"; "\$\${KIND}" #=> "$${KIND}"
I found the following works albeit "ugly"...
eval echo `echo \\$${KIND}`
However when introducing the ID things break once again:
eval echo `echo \\$${KIND}[$ID]`
#> title:5: no matches found: $LIONS[5]
#> no matches found: $LIONS[5]
I feel like I'm missing something very simple. I have a hunch I'm forgetting to escape something, but I'm not quite sure what.
Also, what "less redundant" alternatives to eval echo `echo... or eval echo `print... exist?
In bash, use indirect addressing:
REF="$KIND[$ID]" # Sets REF to "LIONS[5]"
echo "${!REF}" # Prints "Mister Mufasa"
EDIT: In zsh, use nested expansion instead:
echo "${(P)${KIND}[ID]}"

idioms for returning multiple values in shell scripting

Are there any idioms for returning multiple values from a bash function within a script?
http://tldp.org/LDP/abs/html/assortedtips.html describes how to echo multiple values and process the results (e.g., example 35-17), but that gets tricky if some of the returned values are strings with spaces in.
A more structured way to return would be to assign to global variables, like
foo () {
FOO_RV1="bob"
FOO_RV2="bill"
}
foo
echo "foo returned ${FOO_RV1} and ${FOO_RV2}"
I realize that if I need re-entrancy in a shell script I'm probably doing it wrong, but I still feel very uncomfortable throwing global variables around just to hold return values.
Is there a better way? I would prefer portability, but it's probably not a real limitation if I have to specify #!/bin/bash.
In the special case where your values never contain spaces, this read trick can be a simple solution:
get_vars () {
#...
echo "value1" "value2"
}
read var1 var2 < <(get_vars)
echo "var1='$var1', var2='$var2'"
But of course, it breaks as soon as there is a space in one of the values. You could modify IFS and use a special separator in your function's echo, but then the result is not really simpler than the other suggested solutions.
This question was posted 5 years ago, but I have some interesting answer to post. I have just started learning bash, and I also encounter to the same problem as you did. I think this trick might be helpful:
#!/bin/sh
foo=""
bar=""
my_func(){
echo 'foo="a"; bar="b"'
}
eval $(my_func)
echo $foo $bar
# result: a b
This trick is also useful for solving a problem when a child process can not send back a value to its parent process.
Much as I love shell, it's probably the case that as soon as you're throwing arbitrary structured data around, Unix bourne/posix shell is not the right choice.
If there are characters which do not occur inside fields, then separate with one of those. The classic example is /etc/passwd, /etc/group and various other files which use a colon as a field separator.
If using a shell which can handle a NUL character inside strings, then joining on the NUL and separating on it (via $IFS or whatever) can work well. But several common shells, including bash, break on NUL. A test would be an old .sig of mine:
foo=$'a\0b'; [ ${#foo} -eq 3 ] && echo "$0 rocks"
Even if that would work for you, you've just reached one of the warning signs that it's time to switch to a more structured language (Python, Perl, Ruby, Lua, Javascript ... pick your preferred poison). Your code is likely to become hard to maintain; even if you can, there's a smaller pool of people who'll understand it well enough to maintain it.
Yet another way:
function get_tuple()
{
echo -e "Value1\nValue2"
}
IFS=$'\n' read -d '' -ra VALUES < <(get_tuple)
echo "${VALUES[0]}" # Value1
echo "${VALUES[1]}" # Value2
In order version of Bash which doesn't support nameref (introduced in Bash 4.3-alpha) I may define helper function in which the return value is assigned to the given variable. It's sort of like using eval to do the same kind of variable assignment.
Example 1
## Add two complex numbers and returns it.
## re: real part, im: imaginary part.
##
## Helper function named by the 5th positional parameter
## have to have been defined before the function is called.
complexAdd()
{
local re1="$1" im1="$2" re2="$3" im2="$4" fnName="$5" sumRe sumIm
sumRe=$(($re1 + $re2))
sumIm=$(($im1 + $im2))
## Call the function and return 2 values.
"$fnName" "$sumRe" "$sumIm"
}
main()
{
local fooRe='101' fooIm='37' barRe='55' barIm='123' bazRe bazIm quxRe quxIm
## Define the function to receive mutiple return values
## before calling complexAdd().
retValAssign() { bazRe="$1"; bazIm="$2"; }
## Call comlexAdd() for the first time.
complexAdd "$fooRe" "$fooIm" "$barRe" "$barIm" 'retValAssign'
## Redefine the function to receive mutiple return values.
retValAssign() { quxRe="$1"; quxIm="$2"; }
## Call comlexAdd() for the second time.
complexAdd "$barRe" "$barIm" "$bazRe" "$bazIm" 'retValAssign'
echo "foo = $fooRe + $fooIm i"
echo "bar = $barRe + $barIm i"
echo "baz = foo + bar = $bazRe + $bazIm i"
echo "qux = bar + baz = $quxRe + $quxIm i"
}
main
Example 2
## Add two complex numbers and returns it.
## re: real part, im: imaginary part.
##
## Helper functions
## getRetRe(), getRetIm(), setRetRe() and setRetIm()
## have to have been defined before the function is called.
complexAdd()
{
local re1="$1" im1="$2" re2="$3" im2="$4"
setRetRe "$re1"
setRetRe $(($(getRetRe) + $re2))
setRetIm $(($im1 + $im2))
}
main()
{
local fooRe='101' fooIm='37' barRe='55' barIm='123' bazRe bazIm quxRe quxIm
## Define getter and setter functions before calling complexAdd().
getRetRe() { echo "$bazRe"; }
getRetIm() { echo "$bazIm"; }
setRetRe() { bazRe="$1"; }
setRetIm() { bazIm="$1"; }
## Call comlexAdd() for the first time.
complexAdd "$fooRe" "$fooIm" "$barRe" "$barIm"
## Redefine getter and setter functions.
getRetRe() { echo "$quxRe"; }
getRetIm() { echo "$quxIm"; }
setRetRe() { quxRe="$1"; }
setRetIm() { quxIm="$1"; }
## Call comlexAdd() for the second time.
complexAdd "$barRe" "$barIm" "$bazRe" "$bazIm"
echo "foo = $fooRe + $fooIm i"
echo "bar = $barRe + $barIm i"
echo "baz = foo + bar = $bazRe + $bazIm i"
echo "qux = bar + baz = $quxRe + $quxIm i"
}
main
you can make use of associative arrays with you have bash 4 eg
declare -A ARR
function foo(){
...
ARR["foo_return_value_1"]="VAR1"
ARR["foo_return_value_2"]="VAR2"
}
you can combine them as strings.
function foo(){
...
echo "$var1|$var2|$var3"
}
then whenever you need to use those return values,
ret="$(foo)"
IFS="|"
set -- $ret
echo "var1 one is: $1"
echo "var2 one is: $2"
echo "var3 one is: $3"
I would go for the solution I suggested here, but using an array variable instead. Older bash:es don't support associative arrays.
E.g.,
function some_func() # ARRVAR args...
{
local _retvar=$1 # I use underscore to avoid clashes with return variable names
local -a _out
# ... some processing ... (_out[2]=xxx etc.)
eval $_retvar='("${_out[#]}")'
}
Calling site:
function caller()
{
local -a tuple_ret # Do not use leading '_' here.
# ...
some_func tuple_ret "arg1"
printf " %s\n" "${tuple_ret[#]}" # Print tuple members on separate lines
}
Later version of Bash supports nameref. Use declare -n var_name to give var_name the nameref attribute. nameref gives your function the ability to "pass by reference" which is commonly used in C++ functions to return multiple values. According to Bash man page:
A variable can be assigned the nameref attribute using the -n option to the declare or local builtin commands to create a nameref, or a reference to another variable. This allows variables to be manipulated indirectly. Whenever the nameref variable is referenced or assigned to, the operation is actually performed on the variable specified by the nameref variable's value. A nameref is commonly used within shell functions to refer to a variable whose name is passed as an argument to the function.
The following are some interactive command line examples.
Example 1:
$ unset xx yy
$ xx=16
$ yy=xx
$ echo "[$xx] [$yy]"
[16] [xx]
$ declare -n yy
$ echo "[$xx] [$yy]"
[16] [16]
$ xx=80
$ echo "[$xx] [$yy]"
[80] [80]
$ yy=2016
$ echo "[$xx] [$yy]"
[2016] [2016]
$ declare +n yy # Use -n to add and +n to remove nameref attribute.
$ echo "[$xx] [$yy]"
[2016] [xx]
Example 2:
$ func()
> {
> local arg1="$1" arg2="$2"
> local -n arg3ref="$3" arg4ref="$4"
>
> echo ''
> echo 'Local variables:'
> echo " arg1='$arg1'"
> echo " arg2='$arg2'"
> echo " arg3ref='$arg3ref'"
> echo " arg4ref='$arg4ref'"
> echo ''
>
> arg1='1st value of local assignment'
> arg2='2st value of local assignment'
> arg3ref='1st return value'
> arg4ref='2nd return value'
> }
$
$ unset foo bar baz qux
$
$ foo='value of foo'
$ bar='value of bar'
$ baz='value of baz'
$ qux='value of qux'
$
$ func foo bar baz qux
Local variables:
arg1='foo'
arg2='bar'
arg3ref='value of baz'
arg4ref='value of qux'
$
$ {
> echo ''
> echo '2 values are returned after the function call:'
> echo " foo='$foo'"
> echo " bar='$bar'"
> echo " baz='$baz'"
> echo " qux='$qux'"
> }
2 values are returned after the function call:
foo='value of foo'
bar='value of bar'
baz='1st return value'
qux='2nd return value'
I am new to bash, But found this code helping.
function return_multiple_values() {
eval "$1='What is your name'"
eval "$2='my name is: BASH'"
}
return_var=''
res2=''
return_multiple_values return_var res2
echo $return_var
echo $res2
Shell script functions can only return the exit status of last command executed or the exit status of that function specified explicitly by a return statement.
To return some string one way may be this:
function fun()
{
echo "a+b"
}
var=`fun` # Invoke the function in a new child shell and capture the results
echo $var # use the stored result
This may reduce your discomfort although it adds the overhead of creation of a new shell and hence would be marginally slower.

Resources