Charles Duffy's answer works perfectly for Bash 4.3+, but there's no simple solution if you're using an older version of Bash (unless you wish to trifle with eval for some awful reason). However, it can indeed be done!
Here's what I whipped up:
## Arguments:
## $1 - the element to append
## $2 - the name of the array
append_to_array () {
local -ia 'keys=(-1 "${!'$2'[@]}")';
local IFS='';
read -r -d '' -n ${#1} "$2"[${keys[${#keys[@]}-1]}+1] <<< "$1";
}
Explanation:
Indirection can be tricky and took me forever to learn, but it's powerful and fun so I figured I'd explain how everything fits together.
Let's use arr as the name of an array.
When you append elements to an array with something like arr+=(1) or arr+=("first element appended" "second element appended"), the indices(keys) of the elements in the array simply increment by 1 for each element. For example:
$ declare -a arr=(A)
$ arr+=(B)
$ arr+=(C D)
$ declare -p arr
declare -a arr='([0]="A" [1]="B" [2]="C" [3]="D")'
$ echo ${#arr[@]}
4
You can see the size of the array is equal to the array's next available index, but this is not always the case. Continuing on:
$ arr[7648]="E"
$ arr+=(F)
$ echo ${#arr[@]}
6
$ declare -p arr
declare -a arr='([0]="A" [1]="B" [2]="C" [3]="D" [7648]="E" [7649]="F")'
Line 1:
This is why in the first line of my function, I create an integer array, keys, from the indices of a ( ${!arr[@]} expands to the indices of arr. The last element in keys should be 1 less than the index we want to place the new element. However, if arr is unset or empty, ${!arr[@]} will expand to nothing, so I put -1 at the front of the keys to handle this.
Line 2:
Next up, we clear IFS (using local to avoid changing it outside of the function) to make sure any trailing or leading space characters in the appended element are preserved. Without clearing IFS, read and the here string operator <<< will strip leading and trailing space characters from "$1", which is undesirable.
Line 3:
In the third line, we use read to copy the value from "$1" into the array referenced by $2. The -r prevents read from processing/interpreting special characters in "$1" and the -d '' option sets the delimiter to the null character to allow our elements to contain newlines (I will come back to the -n ${#1} option.).
${#keys[@]}-1 evaluates to the index of the last element in keys, so ${keys[${#keys[@]}-1]}+1 grabs the last element of keys and adds one to it, forming our desired index to place "$1".
The read command can be used to write to elements in arrays, e.g. arr[2]="hi" could be replaced with read arr[2] <<< "hi", but read also works with indirect references to arrays, so we could also do nam=arr; read ${nam}[2] <<< "hi" or i=2; nam=arr; read ${nam}[$i] <<< "hi" and produce the same result. This is why read -r -d '' -n ${#1} ${2}[${keys[${#keys[@]}-1]}+1] <<< "$1" is able to append "$1" to the array referenced by $2.
Finally, -n ${#1} is required for reasons unknown to me. When I first wrote the script, every appended element had a newline character appended to it. I do not know why this is, so hopefully someone else can share some insight. So I just worked around this problem by limiting the number of characters read to the number of characters in "$1".
Improved version that can append any number of elements and sanity-checks arguments:
## WARNING: THE ARGUMENTS ARE NOT IN THE SAME ORDER AS THE ABOVE FUNCTION
## $1 - the name of the array
## $2 - the first element to append
## $3-... - optional; can append any number of elements to the array
array_append () {
[[ $# -gt 1 && $1 =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || { 2>&1 echo "invalid args"; return 1; };
local -ia 'k=(-1 "${!'$1'[@]}")';
local n="$1" IFS='';
local -i j=k[${#k[@]}-1] i=1 r=$#;
while ((i<r)); do
shift;
read -r -d '' -n ${#1} "$n"[i+++j] <<< "$1";
done;
}
${#array[@]}to get the next index to write to is buggy. Arrays can be sparse in bash - meaning you can have an arraydeclare -a arr=( [15]=1 ), having only one item, with that item's index being15, not0.${#arr[@]}won't give you the next index it's safe to insert at in such a case.