How to remove an element from a bash array without flattening the array - bash

I would like to make a function that takes a bash array like this one:
a=("element zero" "element one" "element two")
and removes one element like "element one" and leaves a the array like this:
a=("element zero" "element two")
such that echo $a[1] will print out element two and not zero.
I've seen several attempts at this, but haven't found one that did it cleanly or without breaking elements that have spaces into multiple elements. Or just setting the element to be blank (i.e. not shifting the indexes of subsequent array elements).

# initial state
a=( "first element" "second element" "third element" )
# to remove
unset a[0]
# to reindex, such that a[0] is the old a[1], rather than having the array
# start at a[1] with no a[0] entry at all
a=( "${a[#]}" )
# to print the array with its indexes, to check its state at any stage
declare -p a
...now, for a function, if you have bash 4.3, you can use namevars to do this without any eval at all:
remove() {
local -n _arr=$1 # underscore-prefixed name to reduce collision likelihood
local idx=$2
unset _arr[$idx] # remove the undesired item
_arr=( "${_arr[#]}" ) # renumber the indexes
}
For older versions of bash, it's a bit stickier:
remove() {
local cmd
unset "$1[$2]"
printf -v cmd '%q=( "${%q[#]}" )' "$1" "$1" && eval "$cmd"
}
The use of printf with %q format strings is a bit of paranoia -- it makes it harder for maliciously chosen values (in this case, variable names) to perform actions of their choice, as opposed to simply failing with no effect.
All that said -- it's better if you don't renumber your arrays. If you leave off the renumbering step, such that after deleting entry a[1] you simply have a sparse array with no content at that index (which is different from an empty string at that index -- bash "arrays" are actually stored as linked lists or hash tables [in the associative case], not as arrays at all, so sparse arrays are memory-efficient), the delete operation is much faster.
This doesn't break your ability to iterate over your arrays if you retrieve ask the array for its keys rather than supplying them externally, as in:
for key in "${!a[#]}"; do
value="${a[$key]}"
echo "Entry $key has value $value"
done

remove() {
eval "$1=( \"\${$1[#]:0:$2}\" \"\${$1[#]:$(($2+1))}\" )"
}
Can be called like this:
a=("element zero" "element one" "element two")
remove a 1
echo ${a[#]} #element zero element two
echo ${a[1]} #element two
This will leave blank array elements.
a=("element zero" "element one" "" "element three")
remove a 1
echo ${a[#]} #element zero element two
echo ${a[1]} #
echo ${a[2]} #element three
This will flatten unset elements in sparse arrays.

Related

Why is my for loop only storing all desired items as one element?

I am trying to figure out why all items are being stored as one element:
filedates=($dirPath/*.csv)
filebasenames=()
filedates2=()
for file in ${filedates[#]}; do
filebasenames+="${file##*/} "
done
for i in ${filebasenames[#]}; do
filedates2+="$(echo $i | cut -c6-13) "
done
for i in ${filedates2[#]}; do
echo $i
done
echo test here ${filebasenames[0]}
echo test here ${filebasenames[1]}
Im confused because the third for loop prints each element in a new line so I assumed that there is more than one element in the array but when I echo the "test here" line it shows me all the elements in one line indicating that there is only one large string. I verify that with the second echo test here 2 line
You aren't appending to the array; you are only appending to the first element of the array. Appending to an array requires parentheses. (Notice, too, that I've dropped the space from the new array element.)
for file in ${filedates[#]}; do
filebasenames+=("${file##*/}")
done
That said, you don't need a loop at all; you can apply the ## to all the elements of the array in one operation.
filebasenames=("${filedates[#]##*/}")
The other array is probably still best populated using a loop.
for i in "${filebasenames[#]}"; do
filedates2+=("$(echo "$i" | cut -c6-13)")
done

give an array to a function, exclamation mark?

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

Loop over two associative arrays in Bash

Say I have two associative arrays in Bash
declare -A a
declare -A b
a[xz]=1
b[xz]=2
a[zx]=3
b[zx]=4
I want to do something like this
for arr in ${a[#]} ${b[#]}; do echo ${arr[zx]}; done
and get 3 and 4 in output
but I get
$ for arr in ${a[#]} ${b[#]}; do echo ${arr[zx]}; done
1
3
2
4
Is there a way to do this in Bash?
You don't want to iterate over the contents; you want to iterate over the names of the arrays, then use indirect expansion to get the desired value of the fixed key from each array.
for arr in a b; do
t=$arr[zx] # first a[zx], then b[zx]
printf '%s\n' "${!t}"
done
Here, the variable "name" for use in indirect expansion is the name of the array along with the desired index.
Assuming the keys in both the arrays match(a major assumption), you can use one array as reference and loop over the keys and print in each array.
for key in "${!a[#]}"; do
printf "Array-1(%s) %s Array-2(%s) %s\n" "$key" "${a[$key]}" "$key" "${b[$key]}"
done
which produces an output as below. You can of-course remove the fancy debug words(Array-1, Array-2) which was added just for an understanding purpose.
Array-1(xz) 1 Array-2(xz) 2
Array-1(zx) 3 Array-2(zx) 4
One general good practice is always quote (for key in "${!a[#]}") your array expansions in bash, so that the elements are not subjected to word-splitting by the shell.

How to print out individual numbers after ordering an array in Bash?

I'm new to bash and having some issues printing out individual numbers after sorting an array.
I have the following....
for x in ${array[#]}
do
echo $x
done| sort
This is the only way I was able to print out the entire array in order.
Now I'm trying to print out a single item after it's been ordered
so I tried....
for x in ${array[#]}
do
echo ${array[2]}
exit
done| sort
But it prints the third item in the unordered array array instead.
Any help?
You are printing the variable and then trying to sort the ONLY variable that you have printed (in your case ${array[2]})
Try this:
sorted=($(printf '%s\n' "${array[#]}"|sort))
echo ${sorted[2]}
This sorts the array and stores it in another array sorted

bash find keyword in an associative array

I have incoming messages from a chat server that need to be compared against a list of keywords. I was using regular arrays, but would like to switch to associative arrays to try to increase the speed of the processing.
The list of words would be in an array called aWords and the values would be a 'type' indicator, i.e. aWords[damn]="1", with 1 being swear word in a legend to inform the user.
The issue is that I need to compare every index value with the input $line looking for substrings. I'm trying to avoid a loop thru each index value if at all possible.
From http://tldp.org/LDP/abs/html/string-manipulation.html, I'm thinking of the Substring Removal section.
${string#substring}
Deletes shortest match of $substring from front of $string.
A comparison of the 'removed' string from the $line, may help, but will it match also words in the middle of other words? i.e. matching the keyword his inside of this.
Sorry for the long-winded post, but I tried to cover all of what I'm attempting to accomplish as best I could.
# create a colon-separated string of the array keys
# you can do this once, after the array is created.
keys=$(IFS=:; echo "${!aWords[*]}")
if [[ ":$keys:" == *:"$word":* ]]; then
# $word is a key in the array
case ${aWords[$word]} in
1) echo "Tsk tsk: $word is a swear word" ;;
# ...
esac
fi
This is the first time I heard of associative arrays in bash. It inspired me to also try to add something, with the chance ofcourse that I completely miss the point.
Here is a code snippet. I hope I understood how it works:
declare -A SWEAR #create associative array of swearwords (only once)
while read LINE
do
[ "$LINE"] && SWEAR["$LINE"]=X
done < "/path/to/swearword/file"
while :
do
OUTGOING="" #reset output "buffer"
read REST #read a sentence from stdin
while "$REST" #evaluate every word in the sentence
do
WORD=${REST%% *}
REST=${REST#* }
[ ${SWEAR[$WORD]} ] && WORD="XXXX"
OUTGOING="$OUTGOING $WORD"
done
echo "$OUTGOING" #output to stdout
done

Resources