give an array to a function, exclamation mark? - bash

I am trying to give an array to a function. I finish to success to arrive to this solution :
test_arr() {
local a="${!1}"
for i in ${a[#]}
do
echo $i
printf '\n'
done
}
arr=("lol 1" "lol 2" "lol 3");
test_arr arr[#]
However there is two issues with that : there is a copy via the local variable. So I would be able to use $1 directly in the for loop, and I do not understand the purpose of ${!1}. What does mean the !?
Another problem is that for my shell, there is 6 elements instead of 3

If you want to just pass values of an array to a function, you can do this:
test_arr() {
for i in "$#"; do
echo $i
printf '\n'
done
:
}
arr=("lol 1" "lol 2" "lol 3")
test_arr "${arr[#]}"
"${arr[#]}" will pass all values properly delimited to the function where we can access them through $# (all arguments).
! you've asked about is used for indirect reference. I.e. "${!1}" is not value of the first argument, but value of the variable whose name is what the value of the first argument was.
I could have missed something, but it seems like wanting to combine indirection and access all items of indirectly referenced array at the same time would be asking a little too much from shell so I've conjured mighty eval (good reason to start being cautious) to help us out a bit. I've hacked this which allows you to pass array name to a function and then access its items based on that name as seen in the first argument of the function, but it's not pretty and that alone should be enough of a discouragement to not do it. It does create a local variable / array as your example assuming there was some reason to want that.
test_arr() {
local a
eval a=(\"\$\{$1\[#\]\}\")
for i in "${a[#]}"; do
echo $i
done
}
arr=("lol 1" "lol 2" "lol 3")
test_arr arr

Related

How can I create an array whose name includes a variable?

I'm having some trouble writing a command that includes a String of a variable in Bash and wanted to know the correct way to do it.
I want to try and fill the Row arrays with the numbers 1-9 but I'm getting myself stuck when trying to pass a variable Row$Line[$i]=$i.
Row0=()
Row1=()
Row2=()
FillArrays() {
for Line in $(seq 0 2)
do
for i in $(seq 1 9)
do
Row$Line[$i]=$i
done
done
}
I can get the desired result if I echo the command but I assume that is just because it is a String.
I want the for loop to select each row and add the numbers 1-9 in each array.
FillArrays() {
for ((Line=0; Line<8; Line++)); do
declare -g -a "Row$Line" # Ensure that RowN exists as an array
declare -n currRow="Row$Line" # make currRow an alias for that array
for ((i=0; i<9; i++)); do # perform our inner loop...
currRow+=( "$i" ) # ...and populate the target array...
done
unset -n currRow # then clear the alias so it can be reassigned later.
done
}
References:
https://wiki.bash-hackers.org/syntax/ccmd/c_for describes the C-style for loop in bash
BashFAQ #6 discusses indirect reference and assignment in detail, including techniques that precede namevars.
Variable expansion happens too late for an assignment to understand it. You can delay the assignment by using the declare builtin. -g is needed in a function to make the variable global.
Also, you probably don't want to use $Line as the array index, but $i, otherwise you wouldn't populate each line array with numbers 1..9.
#! /bin/bash
Row0=()
Row1=()
Row2=()
FillArrays() {
for Line in $(seq 0 8)
do
for i in $(seq 1 9)
do
declare -g Row$Line[$i]=$i
done
done
}
FillArrays
echo "${Row1[#]}"
But note that using variables as parts of variable names is dangerous. For me, needing this always means I need to switch from the shell to a real programming language.

copy the value of the variable rather than reference bash script

I am a bit new to the bash scripting. So please bear with me. I am trying to create a table and assign the values in for loop like this:
packages=("foo" "bar" "foobar")
packageMap=()
function test() {
i=0;
for package in "${packages[#]}"
do
echo $i
packageMap[$package]=$i
i=$(expr $i + 1)
done
}
test
echo the first value is ${packageMap["foo"]}
The output for this is:
0
1
2
the first value is 2
While my expected output is:
0
1
2
the first value is 0
So basically the variable's reference is being assigned to this rather than the value. How to solve this?
My bash version:
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin16)
TIA
bash 3.2 only has indexed arrays, so packageMap[$package] only works as intended if $package is an integer, not an arbitrary string.
(What you are observing is $package being evaluated in an arithmetic context, where foo, bar, and foobar are recursively expanded until you get an integer value. Undefined variables expand to 0, so packageMap[foo] is equivalent to packageMap[0].)
If you were using bash 4 or later, you could use an associative array:
packages=("foo" "bar" "foobar")
declare -A packageMap
test () {
i=0
for package in "${packages[#]}"
do
echo $i
packageMap[$package]=$i
i=$(($i + 1))
done
}
Given that i is the same as the index of each element of packages, you could also write
test () {
for i in "${!packages[#]}"; do
package=${packages[i]}
packageMap[$package]=$i
done
}
instead of explicitly incrementing i.
As chep says, there are no associative arrays in bash 3. That said, if you don't mind wasting a bit of CPU, you can use functions to similar effect:
#!/bin/bash
packages=("foo" "bar" "foobar")
function packagemap () {
local i
for i in "${!packages[#]}"; do
[[ ${packages[$i]} = $1 ]] && echo "$i" && return
done
echo "unknown"
}
echo "the first value is $(packagemap "foo")"
The ${!array[#]} construct expands to the set of indices for the array, which for a normally non-associative array consist of incrementing integers starting at 0. But array members can be removed without the indices being renumbered (i.e. unset packages[1]), so it's important to be able to refer to actual indices rather than assuming they're sequential with for loop that simply counts.
And I note that you're using Darwin. Remember that you really need it, you can install bash 4 using Homebrew or MacPorts.

read multiple values from a property file using bash shell script

Would like to read multiple values from a property file using a shell script
My properties files looks something like below, the reason I added it following way was to make sure, if in future more students joins I just need to add in in the properties file without changing any thing in the shell script.
student.properties
total_student=6
student_name_1="aaaa"
student_name_2="bbbb"
student_name_3="cccc"
student_name_4="dddd"
student_name_5="eeee"
When I run below script I not getting the desired output, for reading the student names from properties file
student.sh
#!/bin/bash
. /student.properties
i=1
while [ $i -lt $total_student ]
do
{
std_Name=$student_name_$i
echo $std_Name
#****** my logic *******
} || {
echo "ERROR..."
}
i=`expr $i + 1`
done
Output is something like this
1
2
3
4
5
I understand the script is not getting anything for $student_name_ hence only $i value is getting printed.
Hence, wanted to know how to read values from the properties file.
You can do variable name interpolation with ${!foo}. If $foo is "bar", then ${!foo} gives you the value of $bar. In your code that means changing
std_Name=$student_name_$i
to
var=student_name_$i
std_Name=${!var}
Alternatively, you could store the names in an array. Then you wouldn't have to do any parsing.
student.properties
student_names=("aaaa" "bbbb" "cccc" "dddd" "eeee")
student.sh
#!/bin/bash
. /student.properties
for student_name in "${student_names[#]}"; do
...
done
You can use indirect expansion:
std_Name=student_name_$i
echo "${!std_Name}"
the expression ${!var} basically evaluates the variable twice:
first evaluation: student_name_1
second evaluation: foo
Note that this is rarely a good idea and that using an array is almost always preferred.

Create associative array in bash 3

After thoroughly searching for a way to create an associative array in bash, I found that declare -A array will do the trick. But the problem is, it is only for bash version 4 and the bash version the server has in our system is 3.2.16.
How can I achieve some sort of associative array-like hack in bash 3? The values will be passed to a script like
ARG=array[key];
./script.sh ${ARG}
EDIT: I know that I can do this in awk, or other tools but strict bash is needed for the scenario I am trying to solve.
Bash 3 has no associative arrays, so you're going to have to use some other language feature(s) for your purpose. Note that even under bash 4, the code you wrote doesn't do what you claim it does: ./script.sh ${ARG} does not pass the associative array to the child script, because ${ARG} expands to nothing when ARG is an associative array. You cannot pass an associative array to a child process, you need to encode it anyway.
You need to define some argument passing protocol between the parent script and the child script. A common one is to pass arguments in the form key=value. This assumes that the character = does not appear in keys.
You also need to figure out how to represent the associative array in the parent script and in the child script. They need not use the same representation.
A common method to represent an associative array is to use separate variables for each element, with a common naming prefix. This requires that the key name only consists of ASCII letters (of either case), digits and underscores. For example, instead of ${myarray[key]}, write ${myarray__key}. If the key is determined at run time, you need a round of expansion first: instead of ${myarray[$key]}, write
n=myarray__${key}; echo ${!n}
For an assignment, use printf -v. Note the %s format to printf to use the specified value. Do not write printf -v "myarray__${key}" %s "$value" since that would treat $value as a format and perform printf % expansion on it.
printf -v "myarray__${key}" %s "$value"
If you need to pass an associative array represented like this to a child process with the key=value argument representation, you can use ${!myarray__*} to enumerate over all the variables whose name begins with myarray__.
args=()
for k in ${!myarray__*}; do
n=$k
args+=("$k=${!n}")
done
In the child process, to convert arguments of the form key=value to separate variables with a prefix:
for x; do
if [[ $x != *=* ]]; then echo 1>&2 "KEY=VALUE expected, but got $x"; exit 120; fi
printf -v "myarray__${x%%=*}" %s "${x#*=}"
done
By the way, are you sure that this is what you need? Instead of calling a bash script from another bash script, you might want to run the child script in a subshell instead. That way it would inherit from all the variables of the parent.
Here is another post/explanation on associative arrays in bash 3 and older using parameter expansion:
https://stackoverflow.com/a/4444841
Gilles' method has a nice if statement to catch delimiter issues, sanitize oddball input ...etc. Use that.
If you are somewhat familiar with parameter expansion:
http://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
To use in your scenario [ as stated: sending to script ]:
Script 1:
sending_array.sh
# A pretend Python dictionary with bash 3
ARRAY=( "cow:moo"
"dinosaur:roar"
"bird:chirp"
"bash:rock" )
bash ./receive_arr.sh "${ARRAY[#]}"
Script 2: receive_arr.sh
argAry1=("$#")
function process_arr () {
declare -a hash=("${!1}")
for animal in "${hash[#]}"; do
echo "Key: ${animal%%:*}"
echo "Value: ${animal#*:}"
done
}
process_arr argAry1[#]
exit 0
Method 2, sourcing the second script:
Script 1:
sending_array.sh
source ./receive_arr.sh
# A pretend Python dictionary with bash 3
ARRAY=( "cow:moo"
"dinosaur:roar"
"bird:chirp"
"bash:rock" )
process_arr ARRAY[#]
Script 2: receive_arr.sh
function process_arr () {
declare -a hash=("${!1}")
for animal in "${hash[#]}"; do
echo "Key: ${animal%%:*}"
echo "Value: ${animal#*:}"
done
}
References:
Passing arrays as parameters in bash
If you don't want to handle a lot of variables, or keys are simply invalid variable identifiers, and your array is guaranteed to have less than 256 items, you can abuse function return values. This solution does not require any subshell as the value is readily available as a variable, nor any iteration so that performance screams. Also it's very readable, almost like the Bash 4 version.
Here's the most basic version:
hash_index() {
case $1 in
'foo') return 0;;
'bar') return 1;;
'baz') return 2;;
esac
}
hash_vals=("foo_val"
"bar_val"
"baz_val");
hash_index "foo"
echo ${hash_vals[$?]}
More details and variants in this answer
You can write the key-value pairs to a file and then grep by key. If you use a pattern like
key=value
then you can egrep for ^key= which makes this pretty safe.
To "overwrite" a value, just append the new value at the end of the file and use tail -1 to get just the last result of egrep
Alternatively, you can put this information into a normal array using key=value as value for the array and then iterator over the array to find the value.
This turns out to be ridiculously easy. I had to convert a bash 4 script that used a bunch of associative arrays to bash 3. These two helper functions did it all:
array_exp() {
exp=${#//[/__}
eval "${exp//]}"
}
array_clear() {
unset $(array_exp "echo \${!$1__*}")
}
I'm flabbergasted that this actually works, but that's the beauty of bash.
E.g.
((all[ping_lo] += counts[ping_lo]))
becomes
array_exp '((all[ping_lo] += counts[ping_lo]))'
Or this print statement:
printf "%3d" ${counts[ping_lo]} >> $return
becomes
array_exp 'printf "%3d" ${counts[ping_lo]}' >> $return
The only syntax that changes is clearing. This:
counts=()
becomes
array_clear counts
and you're set. You could easily tell array_exp to recognize expressions like "=()" and handle them by rewriting them as array_clear expressions, but I prefer the simplicity of the above two functions.

Iterate over bash arrays, substitute array name dynamically, is this possible?

I have a script that iterates over an array of values, something like this (dumbed down for the purposes of this question) :
COUNTRIES=( ENGLAND SCOTLAND WALES )
for i in ${COUNTRIES[#]}
do
echo "Country is $i "
done
My question is, is it possible to substitute the array dynamically? For example, I want to be able to pass in the array to iterate over at runtime. I've tried the following but I think my syntax might be wrong
COUNTRIES=( ENGLAND SCOTLAND WALES )
ANIMALS=( COW SHEEP DOG )
loopOverSomething()
{
for i in ${$1[#]}
do
echo "value is $i "
done
}
loopOverSomething $ANIMALS
I'm getting line 22: ${$2[#]}: bad substitution
You can use bash's indirect expansion for this:
loopOverSomething()
{
looparray="$1[#]"
for i in "${!looparray}"
do
echo "value is $i"
done
}
This is covered by BashFAQ #006:
We are not aware of any trick that can duplicate that functionality in POSIX or Bourne shells (short of using eval, which is extremely difficult to do securely). Bash can almost do it -- some indirect array tricks work, and others do not, and we do not know whether the syntax involved will remain stable in future releases. So, consider this a use at your own risk hack.
# Bash -- trick #1. Seems to work in bash 2 and up.
realarray=(...) ref=realarray; index=2
tmp="$ref[$index]"
echo "${!tmp}" # gives array element [2]
# Bash -- trick #2. Seems to work in bash 3 and up.
# Does NOT work in bash 2.05b.
tmp="$ref[#]"
printf "<%s> " "${!tmp}"; echo # Iterate whole array.
You could use the array as argument in the following way:
COUNTRIES=( ENGLAND SCOTLAND "NEW WALES" )
ANIMALS=( COW SHEEP DOG )
loopOverSomething()
{
for i in "$#"
do
echo "value is $i "
done
}
loopOverSomething "${ANIMALS[#]}"
loopOverSomething "${COUNTRIES[#]}"

Resources