Inconsistencies when removing elements from a Bash array - arrays

When using bash arrays, you can remove an element in the following manner:
unset array[i] # where i is the array index
The problem with this is after the unset ${array[#]] is not truly valid. Yes
it does give you the number of active array elements, but not the actual array
depth. The index of the removed index in the array still exists. It simply has
been set inactive/null. For example:
declare -a array=( a b c d e )
unset array[2]
for ((i=0; i < ${#array[#]}; i++)) ; do
echo "$i: ${array[$i]}"
done
outputs the following:
0: a
1: b
2:
3: d
array[2] is still there but set to null or inactive
array[4] (e) does not show because ${#array[#]} is the number of active
array elements and not the true array element depth. This gets very messy very
quickly each time an element is unset
As an example consider the following code:
# array contains 0 or more strings
# remove looks for and removes a given string
remove () {
local str=$1
for (( i = 0 ; i < ${#array[#]}; i++ )) ; do
if [[ "${array[$i]}" == "$str" ]] ; then
unset array[$i]
return 0
fi
done
echo "$str: not registered"
return 0
}
This is only valid the first time remove is called. After that, valid
elements may be missed.
One fix for this is to added the following line after the unset:
unset array[$i]
+ array=( "${array[#]}" )
This re-initializes array with the element completely removed.
The problem, this feels kludgy.
So my questions are this:
1) is there a way of getting the true array element depth?
2) is there a way of detecting the end of an array when iterating through it?
3) is there another cleaner solution?

Understanding Why for ((i=0; i<${#array[#]}; i++)) is broken
There's no such thing as "true array element depth" in the sense that you're asking for here. Bash arrays aren't really arrays -- they're hash maps (numerically indexed for regular arrays, indexed by strings for bash 4.0's new "associative arrays"). As such, there's absolutely no reason for the keys to start from 0 -- you can have an array like the following: declare -a array=( [1000]="one thousand" [2000]="two thousand" [3000]="three thousand" ), and its length is exactly 3; there aren't a bunch of NUL/empty elements sitting between those items (looked up with keys 1000, 2000 and 3000, respectively).
Removing Elements From A Sparse Array Safely
Iterate over indices, if you want to remove by index. Whereas "${array[#]}" iterates over items in the array, "${!array[#]}" (note the !) iterates over by the indices by which those items can be looked up.
As you've observed, it's unsafe to assume that the indices range from 0 to the total number of items in an array, as bash arrays are allowed to be sparse -- but there's no reason to write your code to make that assumption in the first place.
for array_idx in "${!array[#]}"; do
unset "array[$array_idx]"
done

Related

Get the index of the first or nth element of sparse bash array

Is there a bash way to get the index of the nth element of a sparse bash array?
printf "%s\t" ${!zArray[#]} | cut -f$N
Using cut to index the indexes of an array seems excessive, especially in reference to the first or last.
If getting the index is only a step towards getting the entry then there is an easy solution: Convert the array into a dense (= non-sparse) array, then access those entries …
sparse=([1]=I [5]=V [10]=X [50]=L)
dense=("${sparse[#]}")
printf %s "${dense[2]}"
# prints X
Or as a function …
nthEntry() {
shift "$1"
shift
printf %s "$1"
}
nthEntry 2 "${sparse[#]}"
# prints X
Assuming (just like you did) that the list of keys "${!sparse[#]}" expands in sorted order (I found neither guarantees nor warnings in bash's manual, therefore I opened another question) this approach can also be used to extract the nth index without external programs like cut.
indices=("${!sparse[#]}")
echo "${indices[2]}"
# prints 10 (the index of X)
nthEntry 2 "${!sparse[#]}"
# prints 10 (the index of X)
If I understood your question correctly, you may use it like this using read:
# sparse array
declare -a arr=([10]="10" [15]="20" [21]="30" [34]="40" [47]="50")
# desired index
n=2
# read all indices into an array
read -ra iarr < <(printf "%s\t" ${!arr[#]})
# fine nth element
echo "${arr[${iarr[n]}]}"
30

How can I assign a range using array length?

This is probably a silly question, more out of curiosity. I have an array in bash:
array=(name1.ext name2.ext name3.ext)
I want to strip off the extension from each element. I was trying to do this by looping over each element, but I was having trouble setting the range of the loop (see below):
for i in 'echo {0..'expr ${#array[#]} - 1'}'; do
newarray[$i]+=$(echo "${array[$i]:0:5}");
done
Please note ' = "back-tick" within the code-block because I wasn't sure how to escape it.
I'm not able to just use a set range (e.g. seq 0 3), because it changes based on the folder, so I wanted to be able to use the length of the array minus 1. I was able to work around this using:
for (( i=0; i<${#array[#]}; i++ )); do
newarray[$i]+=$(echo "${array[$i]:0:5}"); done
But I thought there should be some way to do it with the "array length minus 1" method above and wondered how I was thinking about this incorrectly. Any pointers are appreciated.
Thanks!
Dan
You can apply various parameter expansion operators to each element of an array directly, without needing an explicit loop.
$ array=(name1.ext name2.ext name3.ext)
$ printf '%s\n' "${array[#]%.ext}"
name1
name2
name3
$ newarray=( "${array[#]%.ext}" )
In general, though, there is nearly never any need to generate a range numbers to iterate over. Just use the C-style for loop:
for ((i=0; i< ${#array[#]}; i++ )); do
newarray[$i]="${array[i]%.ext}"
done
With Bash, you could simply loop over your array elements with ${files[#]}:
#!/bin/bash
files=(name1.ext name2.ext name3.ext)
for f in ${files[#]}; do
echo "${f%.*}"
done
Also substring removal ${f%.*} is a better choice if you have extensions of different lengths.
You can use the seq command
for i in `seq 0 $(( ${#array[#]} - 1 ))`
do
···
done
or the bash brace expansion (but in this case you need eval):
for i in `eval echo {0..$(( ${#array[#]} - 1 ))}`
do
···
done
But there is another better (it works even in sparse arrays): let's make bash give us the array indexes:
for i in ${!array[#]}
do
···
done

How to iterate dynamically in an array in bash

I have an array in my script which I want to use it in for, like this:
for j in "${list[#]}"
do
func $j
done
In function func, sometimes another member will add to the list array, but the for iterates as many time as it was initiated(before the for started)
I want "for" to iterate based on the updated array content
some lines of that function:
if [ $s1 -gt 0 ]
then
(( k = $k +1 ))
list[$k]=$id2
fi
As Eric Renouf said, modifying the list you're working on can be tricky. As long as you're only appending new elements (to the end of the list), and just want those new elements included in the iteration, you can use something like this:
for ((i=0; i<${#list[#]}; i++)); do
#...
if (( s1 > 0 )); then
list+=( "$id2" )
fi
done
Since the length of the list (${#list[#]}) is recalculated every time around, the loop will include new elements. Also, the +=( ) syntax guarantees you're always strictly appending.
It appears that k is the index of the last element, meaning your function only appends items to the end of the list. It seems the best option is to iterate while a separate counter is less than k.
i=0
while (( i < k )); do
j=${list[i]}
func "$j"
((i++))
done

How do i create array and put value on it - BASH script

ahhh array and loops my weakest links. I was trying to create array depending on user input so
printf "%s\n" "how may array you want"
read value
after this i will ask what value user want to put on a array(this is the bit im stuck on)
i=1
while [ $i -le $value ]
do
echo "what value you want to put in array $i"
read number
echo $number >> array.db
i=$(( i+1 ))
echo
done
although this method works(i think) but i'm not too sure if i'm actually creating an array and putting value to that array.
you can expand arrays in bash dynamically. you can use this snippet
a=(); a[${#a[#]}]=${number}; echo ${a[#]}
The first statement defines an empty array. with the second (which you can use in your while loop) you insert a value at last elment position + 1, due to ${#a[#]} represents the length of a. the third statement just prints all elements in the array.

Bash: iteration through array names

I am trying to write code to break up a large array into many different small arrays. Eventually the array I would be passed is one of unknown size, this is just my test subject. I have gotten this far:
#!/bin/bash
num=(10 3 12 3 4 4)
inArray=${#num[#]}
numArrays=$(($inArray/2))
remain=$(($inArray%2))
echo $numArrays
echo $remain
nun=0
if test $remain -gt $nun; then
numArrays=$(($numArrays+1))
fi
array=(1 2)
j=0
for ((i=0;i<$numArrays;i++, j=j+2)); do
array=("${num[#]:$j:2}")
echo "The array says: ${array[#]}"
echo "The size? ${#array[#]}"
done
What I am really having a problem with is : I would like to make the variable 'array' be able to change names slightly every time, so each array is kept and has a unique name after the loop. I have tried making the name array_$i but that returns:
[Stephanie#~]$ ./tmp.sh
3
0
./tmp.sh: line 16: syntax error near unexpected token `"${num[#]:$j:2}"'
./tmp.sh: line 16: ` array_$i=("${num[#]:$j:2}")'
[Stephanie#RDT00069 ~]$ ./tmp.sh
3
0
./tmp.sh: line 16: syntax error near unexpected token `$i'
./tmp.sh: line 16: ` array($i)=("${num[#]:$j:2}")'
Does anyone have any advice?
Thanks
I don't think you can really avoid eval here, but you might be able to do it safely if you're careful. Here's my approach:
for name in "${!array_*}"; do # Get all names starting with array_
i="${name#array_*}" # Get the part after array_
if [[ $i != *[^0-9]* ]]; then # Check that it's a number.
printf '%s is not a valid subarray name\n' "$name"
else
# Create a variable named "statement" that contains code you want to eval.
printf -v statement 'cur_array=( "${%s[#]}" )' "$name"
eval "$statement"
# Do interesting things with $cur_array
fi
done
Before this, when you're just creating the array, you know what $name should be, so just use the printf -v part.
To make it even safer, you could save all the allowed array names in another array and check that $name is a member.
With simple variables, you can use the declare keyword to make indirect assignments:
v=foo
declare $v=5
echo $foo # Prints 5
This doesn't extend to arrays in the obvious (to me, anyway) sense:
i=2
# This produces a syntax error
declare -a array_$i=("${num[#]:$j:2}")
Instead, you can declare an empty array
declare -a array_$i
or assign items one at a time:
declare -a array_$i[0]=item1 array_$i[1]=item2
Here's an example of using a for-loop to copy, say, the 3rd
and 4th letters of a big array into a smaller one. We use
i as the dynamic part of the name of the smaller array, and
j as the index into that array.
letters=(a b c d e f)
i=1
j=0
for letter in "${letters[#]:2:2}"; do
# E.g., i=0 and j=1 would result in
# declare -a array_0[1]=c
declare -a array_$i[$j]=$letter
let j+=1
done
done
echo ${array_1[#]}; # c d
${foo[#]:x:y} gives us elements x, x+1, ..., x+y-1 from foo, and
You can wrap the whole thing inside another for-loop to accomplish the goal of splitting letters into 3 smaller arrays:
# We'll create array_0, array_1, and array_2
for i in 0 1 2; do
# Just like our subset above, but start at position i*2 instead of
# a constant.
for letter in "${letters[#]:$((i*2)):2}"; do
declare -a array_$i[$j]=$letter
done
done
Once you manage to populate your three arrays, how do you access them without eval? Bash has syntax for indirect access:
v=foo
foo=5
echo ${!v} # echoes 5!
The exclamation point says to use the word that follows as a variable whose value should be used as the name of the parameter to expand. Knowing that, you might think you could do the following, but you'd be wrong.
i=1
v=array_$i # array_1
echo ${!v[0]} # array_1[0] is c, so prints c, right? Wrong.
In the above, bash tries to find a variable called v[0] and expand it to get the name of a parameter to expand. We actually have to treat our array plus its index as a single name:
i=1
v=array_$i[0]
echo ${!v} # This does print c
This should work, but this is not a good solution, another language may be better bash does not support multi dimensional arrays
eval array_$i='('"${num[#]:$j:2}"')'
And then, for example
eval 'echo "${array_'$i'[0]}"'

Resources