Shell (sh) script array - arrays

Have been trying to find away to do array in sh shell script. Other than BASH etc. have not found much, other than arrays not supported in sh.
Here's what I've come up with using setvar and eval. Is there a better way? Any way to eliminate setvar and/or eval?
#!/bin/sh
# FreeBSD 11.1
# Kind of an array workaround.
echo -e "Simulated array creation and element assignment using dynamic index."
array() {
i=0
for x in $2
do
setvar ${1}_${i} $x # any way to do without setvar?
i=$((i+1))
done
setvar ${1}_cnt $i
}
array "my_arry" "a b c"
echo -e "\nSimulated array element access using dynamic index."
i=0
while [ $i -lt $my_arry_cnt ]
do
eval aev=\${my_arry_${i}} # any way to do without eval?
i=$((i+1))
echo $aev
done
echo -e "\nSimulated array element access using static index."
echo ${my_arry_0}
echo ${my_arry_1}
echo ${my_arry_2}

Related

What is the best way to parse a string from an array to a dictionary?

I receive from "somewhere" an array of stuff that looks like a dictionary
and I need to access the value of a specific one (e.g. the value for the key "FOO")
what is the best way to access it?
Do I need to "parse" the string (or whatever it is) to dictionary or I have a shortcut?
#!/bin/bash
# GNU bash, version 4.4.20
ARR=(FOO='hello' BAR='world')
for index in "${!ARR[#]}"
do
echo "${index}"
I_WISH_WAS_A_DICTIONARY=${ARR[$index]}
echo "${I_WISH_WAS_A_DICTIONARY}"
if [ -v ${I_WISH_WAS_A_DICTIONARY[FOO]} ]; then
break
fi
done
here is a runnable code sample
note about foo bar syntax:
I do not have control over the syntax of FOO='hello', BAR='world'
in the real world those come from dart define
I'm trying to use those in running a "Pre-action" script on a iOS build
function entry_decode() { echo "${*}" | base64 --decode; }
IFS=',' read -r -a define_items <<< "$DART_DEFINES"
for index in "${!define_items[#]}"
do
define_items[$index]=$(entry_decode "${define_items[$index]}");
done
Thanks to #Fravadona for pointing out the limitation of bash on iOS.
Here is a possible solution for whoever may end up in my shoes in the future.
ARR=(FOO=hello BAR=world)
KEY='FOO'
for DEFINE in ${ARR[#]}
do
echo $DEFINE
IFS='=' read -r K V <<< $DEFINE
if [ $K = $KEY ];
then
echo $V
break
fi
done

How can for i in ${VAR} ever work in bash, instead of for i in "${VAR[#]}"?

If I try on my machine (with bash 3,4 and 5) the following command:
bash-5.0$ VAR=(1 2 3)
bash-5.0$ for i in ${VAR}; do echo $i; done
I get only one line with the 1.
If I do the same on ZSH for example, it nicely writes the three lines with progressive numbers.
However in one of our production servers I found this:
bash -c "for i in ${MY_VAR}; do stuff with $i; done"
And by checking the logs it seems that it is actually iterating correctly!
How is this possible? Is it a particular version of bash I’m not aware of? Or some flag I should set? Or maybe the array was populated in a particular way?
It "works" because the code isn't actually using an array at all.
export MY_VAR='1 2 3'
bash -c 'for i in ${MY_VAR}; do echo "Doing stuff with $i"; done'
...involves no arrays whatsoever; MY_VAR is a string being word-split and then glob-expanded.
Don't do that, ever, even if you really do need to iterate over items from a delimiter-separated string. The reliable alternative is to use read -r -a my_array <<<"$MY_VAR" to read your string into an array, and then for i in "${my_array[#]}"; do echo "Doing stuff with $i"; done to iterate over it.
You should write:
var=(1 2 3)
for i in "${var[#]}"; do
do stuff with "$i"
done
You need [#] as shown. And don't use uppercase variable names. Now as to why it works on your production server: possibly because MY_VAR is defined as MY_VAR="1 2 3" (or something analogous), i.e., MY_VAR isn't an array (which is bad).
Looks like Bash evaluates $arr to ${arr[0]}:
arr=(1 2 3)
echo $arr # yields 1
arr[0]=999
echo $arr # yields 999
With associative arrays:
declare -A h
h=([one]=1 [two]=2)
echo $h # yields nothing
h=([0]=1 [two]=2)
echo $h # yields 1
As others have pointed out, the right way to loop through an array is:
for i in "${arr[#]}"; do ...

Loop through dynamically generated array in bash

I'm trying to write a script that performs actions of files with different extensions. To make it as easy to add different actions as possible, the extensions are read from an array, files are found via the "find" command, and results returned to a dynamically generated array named after the file extension that was searched.
To add a new extension to search for I can simply add to the file_ext array.
I create the array like this:
file_exts=("dsd" "dsdd" "dmesg")
for ext in "${file_exts[#]}"
do
echo "Finding $ext files"
eval var="exts_$ext"
declare -a $var="($( find "$dirs_target" -name "*.$ext"))"
done
The arrays are created correctly, and I can manually echo "${exts_dsd[0]} ${exts_dsd[1]}" and see the entries, However, I can't find a way of looping through each entry in the dynamically assigned arrays.
I have tried a few combinations using eval, and I can print out the first entry in the array, IE just referencing "$exts_dsd" Here are two things I've already tried:
for varname in "${!exts_#}"
do
for entry in ${varname[#]}
do
echo "$varname : $entry"
done
eval value=\$${varname[#]}
echo "$varname=$value"
done
How can I loop through each entry in the above for loop, so I can print out all the entries in all the dynamically created arrays?
Here is a complete test script:
#! /bin/bash
file_exts=("dsd" "dsdd" "dmesg")
dirs_target="/tmp/arraytest/"
echo "Creating $dirs_target"
if [[ ! -d "$dirs_target" ]]; then
if ! mkdir "$dirs_target"; then
echo "Couldn't create temp dir"
exit 1
fi
fi
echo "Creating test files"
for tmpfile in $( seq 0 5 )
do
echo -e "\tCreating $dirs_target$tmpfile.dsd"
if ! touch "$dirs_target/$tmpfile.dsd"; then
echo "Coudn't create $dirs_target/test$tmpfile.dsd"
exit 1
fi
done
echo ""
echo "-----Finding Files-----"
for ext in "${file_exts[#]}"
do
echo "Finding $ext files"
eval var="exts_$ext"
declare -a $var="($( find "$dirs_target" -name "*.$ext"))"
done
echo ""
echo "-----File Extensions-----"
for varname in "${!exts_#}"
do
for entry in ${varname[#]}
do
echo "$varname : $entry"
done
eval value=\$${varname[#]}
#echo "$varname=$value"
done
echo ""
echo "Finishing."
rm -rf "$dirs_target"
To loop over the entries, you have to use the same trick as when creating them: just store the variable name in a variable. The point is to include the [#] index, too, which will be correctly recognized in the indirection:
for varname in "${!exts_#}" ; do
arr=$varname'[#]'
for entry in "${!arr}" ; do
echo "$varname : $entry"
done
done
Also note that eval isn't needed in
# eval var="exts_$ext"
var=exts_$ext # works even better!
I've found the answer. I had the eval statement slightly wrong.
echo "-----File Extensions-----"
for varname in "${!exts_#}"
do
echo "varname=$varname"
eval testvalue="\${$varname[#]}"
for entry in $testvalue
do
echo -e "\tFile: $entry"
done
done
As a bonus, I've also figured out how to add to a dynamically created array
var="table_$entry"
declare -a $var
while read -r line
do
eval $var+=\(\'"$line"\'\)
done < "$dirs_table"

Bash : Use a variable as an associative array name

I'm writing a Bash script to simplify file copies from our main site to multiple agencies.
In this script, I'm trying to use a variable as an associative array name but I optain an error, here is the code :
#!/bin/bash
declare -A GROUP1
declare -A GROUP2
declare -A GROUP3
declare -A ARRAY
GROUP1["SITE1"]="x.x.x.x"
GROUP1["SITE2"]="y.y.y.y"
GROUP1["SITE3"]="z.z.z.z"
GROUP2["SITE1"]="1.1.1.1"
GROUP2["SITE2"]="2.2.2.2"
GROUP2["SITE3"]="3.3.3.3"
GROUP2["SITE1"]="a.a.a.a"
GROUP2["SITE2"]="b.b.b.b"
GROUP2["SITE3"]="c.c.c.c"
read -p "Choose a group of sites : " group
case $group in
1 ) DEST="GROUP1" ;;
2 ) DEST="GROUP2" ;;
3 ) DEST="GROUP3" ;;
esac
eval "ARRAY=(\${$DEST[#]})"
for elem in "${!ARRAY[#]}"
do
echo $elem
echo ${ARRAY[$elem]}
done
Here is the error :
./test: line28: TAB : 3.3.3.3 : must use subscript when assigning associative array
./test: line28: TAB : 2.2.2.2 : must use subscript when assigning associative array
./test: line28: TAB : 1.1.1.1 : must use subscript when assigning associative array
Is what I am trying to do possible ?
Thanks in advance.
It's possible but it's not easy unless you have bash v4.3. With 4.3, you can use a "nameref":
declare -A GROUP1
declare -A GROUP2
declare -A GROUP3
GROUP1["SITE1"]="x.x.x.x"
#...
# Yuk. Use command-line options, not stdin
read -p "Choose a group of sites : " group
for g in GROUP1 GROUP2 GROUP3; do if [[ $group == $g ]]; then
# Here's the nameref: After this executes, ARRAY is an alias for
# the indicated variable.
declare -n ARRAY=$group
break
fi
### This is documented but might not work depending on bash version
### Instead you could test for existence of a known key:
### if [[ ! -v ${ARRAY[SITE1]} ]]; then
if [[ ! -R ARRAY ]]; then
echo "No such group" >> /dev/stderr; exit 1
fi
OK, you probably don't yet have bash 4.3 but in the future the above will be useful to someone. So without it, you could follow a strategy like the one which you propose, which is to copy the indicated associative array. That's not too bad if the arrays aren't big. To do that, use the above but replace the nameref line (declare -n ARRAY=$group) with the following:
defn=$(declare -p $group)
eval "${defn/$group/ARRAY}"
unset defn
In this case, you'll have the use the alternative test for success.
Note: It's really not a good idea to use all caps for bash variable names. The convention is that system-generated environment variables and shell builtin variables use all caps. User variables should be lower-cased in order to not collide with these predefined variables.

Looping through an Array in bash

I am currently attempting to create a bash script that will check inside of each users /Library/Mail folder to see if a folder named V2 exists. The script should create an array with each item in the array being a user and then iterate through each of these users checking their home folder for the above captioned contents. This is what I have so far:
#!/bin/bash
cd /Users
array=($(ls))
for i in ${array[#]}
do
if [ -d /$i/Library/Mail/V2 ]
then
echo "$i mail has been upgraded."
else
echo "$i FAIL"
fi
done
Populating your array from the output of ls is going to make for serious problems when you have a username with spaces. Use a glob expression instead. Also, using [ -d $i/... ] will similarly break on names with spaces -- either use [[ -d $i/... ]] (the [[ ]] construct has its own syntax rules and doesn't require quoting) or [ -d "$i/..." ] (with the quotes).
Similarly, you need to double-quote "${array[#]}" to avoid string-splitting from splitting names with spaces in two, as follows:
cd /Users
array=(*)
for i in "${array[#]}"; do
if [[ -d $i/Library/Mail/V2 ]]; then
echo "$i mail has been upgraded."
else
echo "$i FAIL"
fi
done
That said, you don't really need an array here at all:
for i in *; do
...check for $i/Library/Mail/V2...
done

Resources