How can I assign a range using array length? - arrays

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

Related

How to use a variable's value as identifier in order to declare/modify an array in BASH?

I'll try to explain:
I'm writing a bash script and I'm within a for loop. For all loops, I got a variable VAR_ID, that is used to store an identifier for another variable that will be exported when all work is done. For each single loop, I got a variable VAL. VAL's value should be assigned to VAR_ID's value. In the end, VAR_ID's value has to be an array in order to store all values.
Phew... well, it's a bit hard to explain for me, but I think this simple script should clarify what I'm trying to do (it's narrowed down from its actual purpose of course):
#!/bin/bash
COUNT=0
VAR_ID="my_array"
declare -a "$VAR_ID"
while (( $COUNT <= 2 ))
do
VAL=$RANDOM
$VAR_ID+=( "$VAL" ) # this doesn't work
(( COUNT++ ))
done
export $VAR_ID
This should result in a variable my_array and three random values in it. I tried using declare as in declare $VAR_ID=$VAL, but this doesn't work anymore if I use += instead of =.
COUNT can be used in a possible solution as position number or so if it helps as I have it also in my original script.
Thanks for any help in advance
Edit: I know there is a possibility with eval but I try to avoid using that until there is no other way.
I'm not sure what you're trying to do exactly, but you could use an associative array and export that. You can access elements of an associative array with variables in bash.
#!/usr/bin/env bash
declare -Ax arr # declare and export an associative array named 'arr'
declare -x var_id='array_position' # declare and export a variable named 'var_id'
for((i=0; i<=2; i++)); do
val=${RANDOM}
arr["${var_id}"]="${val}" # assign to the associative array with the var_id variable being the key, and val being the value
done
You can then access the associative array with variables, strings, or by iterating through it.
# Using an exact key name
echo "${arr[array_position]}"
# Using a variable which points to the key name
echo "${arr[${var_id}]}"
# Using a for loop
for key in "${!arr[#]}"; do
value="${arr[${key}]}"
echo "${key} ${value}"
done
From the bash manpage
When assigning to an associative array, the words in a compound
assignment may be either assignment statements, for which the
subscript is required, or a list of words that is interpreted as a
sequence of alternating keys and values: name=(key1 value1 key2 value2
… ). These are treated identically to name=( [key1]=value1
[key2]=value2 … ). The first word in the list determines how the
remaining words are interpreted; all assignments in a list must be of
the same type. When using key/value pairs, the keys may not be missing
or empty; a final missing value is treated like the empty string.
This syntax is also accepted by the declare builtin. Individual array
elements may be assigned to using the name[subscript]=value syntax
introduced above.
Here is an answer for my own question for anyone who encounters the same problem:
#!/bin/bash
COUNT=0
VAR_ID="my_array"
declare PSEUDOARRAY # is actually just a list of strings separated by spaces
while (( $COUNT <= 2 ))
do
VAL=$RANDOM
PSEUDOARRAY+="$VAL "
(( COUNT++ ))
done
declare -agx $VAR_ID='( $PSEUDOARRAY )' # "cast" PSEUDOARRAY on an actual array
This solves my problem. Note that if you don't do the "cast" part at the end an just use VAR_ID directly, the resulting my_array doesn't provide things like ${my_array[0]} to give only the first item or ${#my_array[#]} to count the items.
Also note: This method is limited as it does not distinguish between separator spaces and "value spaces" that may be stored in VAL.

Inconsistencies when removing elements from a Bash array

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

Output on one line all elements of an array that satisfy a condition using BASH

The following is to be implemented in a BASH script. I have access to what I think are other "devices" commonly used in bash scripts such as sed, awk, bc, dc etc. I have an array whose elements are strings. The elements are either "none" or a name. This question is in 2 parts. The first goes over the simplest form of the problem with as little background as possible. It is all that is needed I think. The second part is almost entirely background on what I am trying to do in general and may be useful, but I do not think is needed necessarily.
Part 1: SHORT VERSION
I have an array of strings
An example of this array is
array=('none' 'none' 'name1' 'none' 'name2' 'none' 'name3' 'none' 'name4')
In my problem the array is much longer. I would like to search if specific elements are 'none' or 'name#' (i.e., not 'none'). AND What I need is to start at element i and check every nth element after that to see if it is 'none' or 'name#' and print all elements that are 'name#' on one line
For this example that means if I want to output starting from say element 2 (array starts at 0) and then checking every 3rd element after that:
my output for this would be name1 name4
if I had started at element 1 then my output would have been name3
I do not understand piping sufficiently well to be able to echo with a condition (2 conditions really, the first being the periodic range of elements, the second the name vs none criteria).
Part 2: LONGER VERSION (NOT NECCESSARY)
I am running a double do loop that calls a program. every once in a while the output is not what I want, so I flag the run name and part of that run that I didn't like. I have this criteria set up it saves the "i" "j" in the name so I know where the failure occurred.
Since bash does arrays and not matrices, I save these failed states in an array that is i * j long. This is why I need to access a certain section. If each run has 3 parts, then in post analysis, for that run, I search the elements that correspond to that loop. i.e. if there are 3 runs with 3 parts in each, then in my array elements 0-2 are run 1, 3-5 are run 2, and 6-8 are run 3. This corresponds to the array I have shown above.
each run has a part 1, 2 and 3 in this example, or in bash syntax a part 0,1 and 2. (i.e. elements 0,3,6 is the first parts of runs 0 1 2 respectively in the above array, 1,4,7 is the second parts of runs 0,1,2 respectively, and elements 2,5,8 are the 3rd parts of runs 0,1,2 respectively). For each set of parts I want to print out all elements corresponding to those parts that are not 'none'
Here is a dummy code snippet of the process (and this works fine, the question pertains to the post analysis part after)
k=0
for ((i=0 ; i<$NRuns ; i++)); do
for ((j=0 ; j<$NPartsPerRun ; j++)); do
k=$((k + 1))
#PSEUDO CODE HERE not actual bash
call program
if "program output bad" ; then
array[$k]=$Runname
else
echo "Thanks for taking the time to look at my problem"
fi
done
done
Now for post analysis, I want to know for a given part, which runs failed. i.e for a given "j" what were the runnames of the "i's" that failed
Post analysis (and my issue. The code is a sorry attempt to convey the idea, it is really more of a pseudo code, it is not bash)
for ((i=0 ; i<$NPartsPerRun ; i++)); do
#PSEUDO CODE HERE not actual bash
#first element to check is $i, then check every Xth element
if ${array[$i]} || ${array[$(i+X)]} etc.. != 'none' echo "element names that pass condition test"
fi
done
done
If I understood the question correctly, you could emulate a matrix with an associative array and make your code much more readable:
declare -A matrix=()
# save runs into the matrix
for ((i=0; i<n_runs; i++)); do
for ((j=0; j<n_parts; j++)); do
[[ $(program) = bad ]] && matrix[$i,$j]=$name
done
done
# print unsuccessful runs for a given part
part=2 runs=
for ((i=0; i<n_runs; i++)); do
[[ ${matrix[$i,$part]} ]] && runs+="${matrix[$i,$part]} "
done
printf '%s\n' "${runs% }"
I don't know if it solves your problem but I think that it would be something close to this.
array=('none' 'none' 'name1' 'none' 'name2' 'none' 'name3' 'none' 'name4')
step=3
echo "array size = ${#array[#]}"
echo "step = $step"
for ((i= 0; i < step; i += 1)); do
echo -ne "i = $i: [ "
for ((j = i; j < ${#array[#]}; j += step)); do
if [[ "${array[$j]}" != "none" ]]; then
echo -n "${array[$j]} "
fi
done
echo "]"
done
This will output:
array size = 9
step = 3
i = 0: [ name3 ]
i = 1: [ name2 ]
i = 2: [ name1 name4 ]
This works by using a nested loop to check for the indexes of the form $i + n * $step.
You could, of course, change the way it's printing the values to suit your use case. I hope it answers at least the short version of your question!

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]}"'

Passing arrays as parameters in bash

How can I pass an array as parameter to a bash function?
Note: After not finding an answer here on Stack Overflow, I posted my somewhat crude solution myself. It allows for only one array being passed, and it being the last element of the parameter list. Actually, it is not passing the array at all, but a list of its elements, which are re-assembled into an array by called_function(), but it worked for me. If someone knows a better way, feel free to add it here.
You can pass multiple arrays as arguments using something like this:
takes_ary_as_arg()
{
declare -a argAry1=("${!1}")
echo "${argAry1[#]}"
declare -a argAry2=("${!2}")
echo "${argAry2[#]}"
}
try_with_local_arys()
{
# array variables could have local scope
local descTable=(
"sli4-iread"
"sli4-iwrite"
"sli3-iread"
"sli3-iwrite"
)
local optsTable=(
"--msix --iread"
"--msix --iwrite"
"--msi --iread"
"--msi --iwrite"
)
takes_ary_as_arg descTable[#] optsTable[#]
}
try_with_local_arys
will echo:
sli4-iread sli4-iwrite sli3-iread sli3-iwrite
--msix --iread --msix --iwrite --msi --iread --msi --iwrite
Edit/notes: (from comments below)
descTable and optsTable are passed as names and are expanded in the function. Thus no $ is needed when given as parameters.
Note that this still works even with descTable etc being defined with local, because locals are visible to the functions they call.
The ! in ${!1} expands the arg 1 variable.
declare -a just makes the indexed array explicit, it is not strictly necessary.
Note: This is the somewhat crude solution I posted myself, after not finding an answer here on Stack Overflow. It allows for only one array being passed, and it being the last element of the parameter list. Actually, it is not passing the array at all, but a list of its elements, which are re-assembled into an array by called_function(), but it worked for me. Somewhat later Ken posted his solution, but I kept mine here for "historic" reference.
calling_function()
{
variable="a"
array=( "x", "y", "z" )
called_function "${variable}" "${array[#]}"
}
called_function()
{
local_variable="${1}"
shift
local_array=("${#}")
}
Commenting on Ken Bertelson solution and answering Jan Hettich:
How it works
the takes_ary_as_arg descTable[#] optsTable[#] line in try_with_local_arys() function sends:
This is actually creates a copy of the descTable and optsTable arrays which are accessible to the takes_ary_as_arg function.
takes_ary_as_arg() function receives descTable[#] and optsTable[#] as strings, that means $1 == descTable[#] and $2 == optsTable[#].
in the beginning of takes_ary_as_arg() function it uses ${!parameter} syntax, which is called indirect reference or sometimes double referenced, this means that instead of using $1's value, we use the value of the expanded value of $1, example:
baba=booba
variable=baba
echo ${variable} # baba
echo ${!variable} # booba
likewise for $2.
putting this in argAry1=("${!1}") creates argAry1 as an array (the brackets following =) with the expanded descTable[#], just like writing there argAry1=("${descTable[#]}") directly.
the declare there is not required.
N.B.: It is worth mentioning that array initialization using this bracket form initializes the new array according to the IFS or Internal Field Separator which is by default tab, newline and space. in that case, since it used [#] notation each element is seen by itself as if he was quoted (contrary to [*]).
My reservation with it
In BASH, local variable scope is the current function and every child function called from it, this translates to the fact that takes_ary_as_arg() function "sees" those descTable[#] and optsTable[#] arrays, thus it is working (see above explanation).
Being that case, why not directly look at those variables themselves? It is just like writing there:
argAry1=("${descTable[#]}")
See above explanation, which just copies descTable[#] array's values according to the current IFS.
In summary
This is passing, in essence, nothing by value - as usual.
I also want to emphasize Dennis Williamson comment above: sparse arrays (arrays without all the keys defines - with "holes" in them) will not work as expected - we would loose the keys and "condense" the array.
That being said, I do see the value for generalization, functions thus can get the arrays (or copies) without knowing the names:
for ~"copies": this technique is good enough, just need to keep aware, that the indices (keys) are gone.
for real copies:
we can use an eval for the keys, for example:
eval local keys=(\${!$1})
and then a loop using them to create a copy.
Note: here ! is not used it's previous indirect/double evaluation, but rather in array context it returns the array indices (keys).
and, of course, if we were to pass descTable and optsTable strings (without [#]), we could use the array itself (as in by reference) with eval. for a generic function that accepts arrays.
The basic problem here is that the bash developer(s) that designed/implemented arrays really screwed the pooch. They decided that ${array} was just short hand for ${array[0]}, which was a bad mistake. Especially when you consider that ${array[0]} has no meaning and evaluates to the empty string if the array type is associative.
Assigning an array takes the form array=(value1 ... valueN) where value has the syntax [subscript]=string, thereby assigning a value directly to a particular index in the array. This makes it so there can be two types of arrays, numerically indexed and hash indexed (called associative arrays in bash parlance). It also makes it so that you can create sparse numerically indexed arrays. Leaving off the [subscript]= part is short hand for a numerically indexed array, starting with the ordinal index of 0 and incrementing with each new value in the assignment statement.
Therefore, ${array} should evaluate to the entire array, indexes and all. It should evaluate to the inverse of the assignment statement. Any third year CS major should know that. In that case, this code would work exactly as you might expect it to:
declare -A foo bar
foo=${bar}
Then, passing arrays by value to functions and assigning one array to another would work as the rest of the shell syntax dictates. But because they didn't do this right, the assignment operator = doesn't work for arrays, and arrays can't be passed by value to functions or to subshells or output in general (echo ${array}) without code to chew through it all.
So, if it had been done right, then the following example would show how the usefulness of arrays in bash could be substantially better:
simple=(first=one second=2 third=3)
echo ${simple}
the resulting output should be:
(first=one second=2 third=3)
Then, arrays could use the assignment operator, and be passed by value to functions and even other shell scripts. Easily stored by outputting to a file, and easily loaded from a file into a script.
declare -A foo
read foo <file
Alas, we have been let down by an otherwise superlative bash development team.
As such, to pass an array to a function, there is really only one option, and that is to use the nameref feature:
function funky() {
local -n ARR
ARR=$1
echo "indexes: ${!ARR[#]}"
echo "values: ${ARR[#]}"
}
declare -A HASH
HASH=([foo]=bar [zoom]=fast)
funky HASH # notice that I'm just passing the word 'HASH' to the function
will result in the following output:
indexes: foo zoom
values: bar fast
Since this is passing by reference, you can also assign to the array in the function. Yes, the array being referenced has to have a global scope, but that shouldn't be too big a deal, considering that this is shell scripting. To pass an associative or sparse indexed array by value to a function requires throwing all the indexes and the values onto the argument list (not too useful if it's a large array) as single strings like this:
funky "${!array[*]}" "${array[*]}"
and then writing a bunch of code inside the function to reassemble the array.
DevSolar's answer has one point I don't understand (maybe he has a specific reason to do so, but I can't think of one): He sets the array from the positional parameters element by element, iterative.
An easier approuch would be
called_function()
{
...
# do everything like shown by DevSolar
...
# now get a copy of the positional parameters
local_array=("$#")
...
}
An easy way to pass several arrays as parameter is to use a character-separated string. You can call your script like this:
./myScript.sh "value1;value2;value3" "somethingElse" "value4;value5" "anotherOne"
Then, you can extract it in your code like this:
myArray=$1
IFS=';' read -a myArray <<< "$myArray"
myOtherArray=$3
IFS=';' read -a myOtherArray <<< "$myOtherArray"
This way, you can actually pass multiple arrays as parameters and it doesn't have to be the last parameters.
function aecho {
set "$1[$2]"
echo "${!1}"
}
Example
$ foo=(dog cat bird)
$ aecho foo 1
cat
Modern bash (apparently version 4.3 or later), allows you to pass arrays by reference. I'll show that below. If you'd like to manually serialize and deserialize the arrays instead, see my answer here for bash regular "indexed" arrays, and here for bash associative arrays. Passing arrays by reference, as shown below, is much easier and more-concise, however, so that's what I now recommend.
The code below is also available online in my eRCaGuy_hello_world repo here: array_pass_as_bash_parameter_by_reference.sh. See also this example here: array_pass_as_bash_parameter_2_associative.sh.
Here is a demo for regular bash arrays:
function foo {
# declare a local **reference variable** (hence `-n`) named `data_ref`
# which is a reference to the value stored in the first parameter
# passed in
local -n data_ref="$1"
echo "${data_ref[0]}"
echo "${data_ref[1]}"
}
# declare a regular bash "indexed" array
declare -a data
data+=("Fred Flintstone")
data+=("Barney Rubble")
foo "data"
Sample output:
Fred Flintstone
Barney Rubble
...and here is a demo for associative bash arrays (ie: bash hash tables, "dictionaries", or "unordered maps"):
function foo {
# declare a local **reference variable** (hence `-n`) named `data_ref`
# which is a reference to the value stored in the first parameter
# passed in
local -n data_ref="$1"
echo "${data_ref["a"]}"
echo "${data_ref["b"]}"
}
# declare a bash associative array
declare -A data
data["a"]="Fred Flintstone"
data["b"]="Barney Rubble"
foo "data"
Sample output:
Fred Flintstone
Barney Rubble
References:
I modified the above code samples from #Todd Lehman's answer here: How to pass an associative array as argument to a function in Bash?
See also my manual serializing/deserializing answer here
And see my follow-up Question here: Why do the man bash pages state the declare and local -n attribute "cannot be applied to array variables", and yet it can?
This one works even with spaces:
format="\t%2s - %s\n"
function doAction
{
local_array=("$#")
for (( i = 0 ; i < ${#local_array[#]} ; i++ ))
do
printf "${format}" $i "${local_array[$i]}"
done
echo -n "Choose: "
option=""
read -n1 option
echo ${local_array[option]}
return
}
#the call:
doAction "${tools[#]}"
With a few tricks you can actually pass named parameters to functions, along with arrays.
The method I developed allows you to access parameters passed to a function like this:
testPassingParams() {
#var hello
l=4 #array anArrayWithFourElements
l=2 #array anotherArrayWithTwo
#var anotherSingle
#reference table # references only work in bash >=4.3
#params anArrayOfVariedSize
test "$hello" = "$1" && echo correct
#
test "${anArrayWithFourElements[0]}" = "$2" && echo correct
test "${anArrayWithFourElements[1]}" = "$3" && echo correct
test "${anArrayWithFourElements[2]}" = "$4" && echo correct
# etc...
#
test "${anotherArrayWithTwo[0]}" = "$6" && echo correct
test "${anotherArrayWithTwo[1]}" = "$7" && echo correct
#
test "$anotherSingle" = "$8" && echo correct
#
test "${table[test]}" = "works"
table[inside]="adding a new value"
#
# I'm using * just in this example:
test "${anArrayOfVariedSize[*]}" = "${*:10}" && echo correct
}
fourElements=( a1 a2 "a3 with spaces" a4 )
twoElements=( b1 b2 )
declare -A assocArray
assocArray[test]="works"
testPassingParams "first" "${fourElements[#]}" "${twoElements[#]}" "single with spaces" assocArray "and more... " "even more..."
test "${assocArray[inside]}" = "adding a new value"
In other words, not only you can call your parameters by their names (which makes up for a more readable core), you can actually pass arrays (and references to variables - this feature works only in bash 4.3 though)! Plus, the mapped variables are all in the local scope, just as $1 (and others).
The code that makes this work is pretty light and works both in bash 3 and bash 4 (these are the only versions I've tested it with). If you're interested in more tricks like this that make developing with bash much nicer and easier, you can take a look at my Bash Infinity Framework, the code below was developed for that purpose.
Function.AssignParamLocally() {
local commandWithArgs=( $1 )
local command="${commandWithArgs[0]}"
shift
if [[ "$command" == "trap" || "$command" == "l="* || "$command" == "_type="* ]]
then
paramNo+=-1
return 0
fi
if [[ "$command" != "local" ]]
then
assignNormalCodeStarted=true
fi
local varDeclaration="${commandWithArgs[1]}"
if [[ $varDeclaration == '-n' ]]
then
varDeclaration="${commandWithArgs[2]}"
fi
local varName="${varDeclaration%%=*}"
# var value is only important if making an object later on from it
local varValue="${varDeclaration#*=}"
if [[ ! -z $assignVarType ]]
then
local previousParamNo=$(expr $paramNo - 1)
if [[ "$assignVarType" == "array" ]]
then
# passing array:
execute="$assignVarName=( \"\${#:$previousParamNo:$assignArrLength}\" )"
eval "$execute"
paramNo+=$(expr $assignArrLength - 1)
unset assignArrLength
elif [[ "$assignVarType" == "params" ]]
then
execute="$assignVarName=( \"\${#:$previousParamNo}\" )"
eval "$execute"
elif [[ "$assignVarType" == "reference" ]]
then
execute="$assignVarName=\"\$$previousParamNo\""
eval "$execute"
elif [[ ! -z "${!previousParamNo}" ]]
then
execute="$assignVarName=\"\$$previousParamNo\""
eval "$execute"
fi
fi
assignVarType="$__capture_type"
assignVarName="$varName"
assignArrLength="$__capture_arrLength"
}
Function.CaptureParams() {
__capture_type="$_type"
__capture_arrLength="$l"
}
alias #trapAssign='Function.CaptureParams; trap "declare -i \"paramNo+=1\"; Function.AssignParamLocally \"\$BASH_COMMAND\" \"\$#\"; [[ \$assignNormalCodeStarted = true ]] && trap - DEBUG && unset assignVarType && unset assignVarName && unset assignNormalCodeStarted && unset paramNo" DEBUG; '
alias #param='#trapAssign local'
alias #reference='_type=reference #trapAssign local -n'
alias #var='_type=var #param'
alias #params='_type=params #param'
alias #array='_type=array #param'
Just to add to the accepted answer, as I found it doesn't work well if the array contents are someting like:
RUN_COMMANDS=(
"command1 param1... paramN"
"command2 param1... paramN"
)
In this case, each member of the array gets split, so the array the function sees is equivalent to:
RUN_COMMANDS=(
"command1"
"param1"
...
"command2"
...
)
To get this case to work, the way I found is to pass the variable name to the function, then use eval:
function () {
eval 'COMMANDS=( "${'"$1"'[#]}" )'
for COMMAND in "${COMMANDS[#]}"; do
echo $COMMAND
done
}
function RUN_COMMANDS
Just my 2©
As ugly as it is, here is a workaround that works as long as you aren't passing an array explicitly, but a variable corresponding to an array:
function passarray()
{
eval array_internally=("$(echo '${'$1'[#]}')")
# access array now via array_internally
echo "${array_internally[#]}"
#...
}
array=(0 1 2 3 4 5)
passarray array # echo's (0 1 2 3 4 5) as expected
I'm sure someone can come up with a clearner implementation of the idea, but I've found this to be a better solution than passing an array as "{array[#]"} and then accessing it internally using array_inside=("$#"). This becomes complicated when there are other positional/getopts parameters. In these cases, I've had to first determine and then remove the parameters not associated with the array using some combination of shift and array element removal.
A purist perspective likely views this approach as a violation of the language, but pragmatically speaking, this approach has saved me a whole lot of grief. On a related topic, I also use eval to assign an internally constructed array to a variable named according to a parameter target_varname I pass to the function:
eval $target_varname=$"(${array_inside[#]})"
Hope this helps someone.
My short answer is:
function display_two_array {
local arr1=$1
local arr2=$2
for i in $arr1
do
echo "arrary1: $i"
done
for i in $arr2
do
echo "arrary2: $i"
done
}
test_array=(1 2 3 4 5)
test_array2=(7 8 9 10 11)
display_two_array "${test_array[*]}" "${test_array2[*]}"
It should be noticed that the ${test_array[*]} and ${test_array2[*]} should be surrounded by "", otherwise you'll fail.
The answer below shows you how to pass bash regular "indexed" arrays as parameters to a function essentially by serializing and deserializing them.
To see this manual serializing/deserializing for bash associative arrays (hash tables) instead of for regular indexed arrays, see my answer here.
For a better way (which requires bash version 4.3 or later, I think), which passes the arrays by reference, see the link just above and my other answer here.
Passing arrays by reference is much easier and more-concise, so that's what I now recommend. That being said, the manual serializing/deserializing techniques I show below are also extremely informative and useful.
Quick summary:
See the 3 separate function definitions below. I go over how to pass:
one bash array to a function
two or more bash arrays to a function, and
two or more bash arrays plus additional arguments (before or after the arrays) to a function.
12 years later and I still don't see any answers I really like here and which I would consider to be thorough enough, simple enough, and "canonical" enough for me to just use--answers which I can come back to again and again and copy and paste and expand when needed. So, here is my answer which I do consider to be all of these things.
How to pass bash arrays as parameters to bash functions
You might also call this "variadic argument parsing in bash functions or scripts", especially since the number of elements in each array passed in to the examples below can dynamically vary, and in bash the elements of an array essentially get passed to the function as separate input parameters even when the array is passed in via a single array expansion argument like this "${array1[#]}".
For all example code below, assume you have these two bash arrays for testing:
array1=()
array1+=("one")
array1+=("two")
array1+=("three")
array2=("four" "five" "six" "seven" "eight")
The code above and below is available in my bash/array_pass_as_bash_parameter.sh file in my eRCaGuy_hello_world repo on GitHub.
Example 1: how to pass one bash array to a function
To pass an array to a bash function, you have to pass all of its elements separately. Given bash array array1, the syntax to obtain all elements of this array is "${array1[#]}". Since all incoming parameters to a bash function or executable file get wrapped up in the magic bash input parameter array called #, you can read all members of the input array with the "$#" syntax, as shown below.
Function definition:
# Print all elements of a bash array.
# General form:
# print_one_array array1
# Example usage:
# print_one_array "${array1[#]}"
print_one_array() {
for element in "$#"; do
printf " %s\n" "$element"
done
}
Example usage:
echo "Printing array1"
# This syntax passes all members of array1 as separate input arguments to
# the function
print_one_array "${array1[#]}"
Example Output:
Printing array1
one
two
three
Example 2: how to pass two or more bash arrays to a function...
(and how to recapture the input arrays as separate bash arrays again)
Here, we need to differentiate which incoming parameters belong to which array. To do this, we need to know the size of each array, meaning the number of elements in each array. This is very similar to passing arrays in C, where we also generally must know the array length passed to any C function. Given bash array array1, the number of elements in it can be obtained with "${#array1[#]}" (notice the usage of the # symbol). In order to know where in the input arguments the array_len length parameter is, we must always pass the array length parameter for each array before passing the individual array elements, as shown below.
In order to parse the arrays, I use array slicing on the input argument array, #.
Here is a reminder on how bash array slicing syntax works (from my answer here). In the slicing syntax :start:length, the 1st number is the zero-based index to start slicing from, and the 2nd number is the number of elements to grab:
# array slicing basic format 1: grab a certain length starting at a certain
# index
echo "${#:2:5}"
# │ │
# │ └────> slice length
# └──────> slice starting index (zero-based)
# array slicing basic format 2: grab all remaining array elements starting at a
# certain index through to the end
echo "${#:2}"
# │
# │
# └──────> slice starting index (zero-based)
Also, in order to force the sliced parameters from the input array to become a new array, I surround them in parenthesis (), like this, for example ("${#:$i:$array1_len}"). Those parenthesis on the outside are important, again, because that's how we make an array in bash.
This example below only accepts two bash arrays, but following the given patterns it can be easily adapted to accept any number of bash arrays as arguments.
Function definition:
# Print all elements of two bash arrays.
# General form (notice length MUST come before the array in order
# to be able to parse the args!):
# print_two_arrays array1_len array1 array2_len array2
# Example usage:
# print_two_arrays "${#array1[#]}" "${array1[#]}" \
# "${#array2[#]}" "${array2[#]}"
print_two_arrays() {
# For debugging: print all input args
echo "All args to 'print_two_arrays':"
print_one_array "$#"
i=1
# Read array1_len into a variable
array1_len="${#:$i:1}"
((i++))
# Read array1 into a new array
array1=("${#:$i:$array1_len}")
((i += $array1_len))
# Read array2_len into a variable
array2_len="${#:$i:1}"
((i++))
# Read array2 into a new array
array2=("${#:$i:$array2_len}")
((i += $array2_len))
# Print the two arrays
echo "array1:"
print_one_array "${array1[#]}"
echo "array2:"
print_one_array "${array2[#]}"
}
Example usage:
echo "Printing array1 and array2"
print_two_arrays "${#array1[#]}" "${array1[#]}" "${#array2[#]}" "${array2[#]}"
Example Output:
Printing array1 and array2
All args to 'print_two_arrays':
3
one
two
three
5
four
five
six
seven
eight
array1:
one
two
three
array2:
four
five
six
seven
eight
Example 3: pass two bash arrays plus some extra args after that to a function
This is a tiny expansion of the example above. It also uses bash array slicing, just like the example above. Instead of stopping after parsing two full input arrays, however, we continue and parse a couple more arguments at the end. This pattern can be continued indefinitely for any number of bash arrays and any number of additional arguments, accommodating any input argument order, so long as the length of each bash array comes just before the elements of that array.
Function definition:
# Print all elements of two bash arrays, plus two extra args at the end.
# General form (notice length MUST come before the array in order
# to be able to parse the args!):
# print_two_arrays_plus_extra_args array1_len array1 array2_len array2 \
# extra_arg1 extra_arg2
# Example usage:
# print_two_arrays_plus_extra_args "${#array1[#]}" "${array1[#]}" \
# "${#array2[#]}" "${array2[#]}" "hello" "world"
print_two_arrays_plus_extra_args() {
i=1
# Read array1_len into a variable
array1_len="${#:$i:1}"
((i++))
# Read array1 into a new array
array1=("${#:$i:$array1_len}")
((i += $array1_len))
# Read array2_len into a variable
array2_len="${#:$i:1}"
((i++))
# Read array2 into a new array
array2=("${#:$i:$array2_len}")
((i += $array2_len))
# You can now read the extra arguments all at once and gather them into a
# new array like this:
extra_args_array=("${#:$i}")
# OR you can read the extra arguments individually into their own variables
# one-by-one like this
extra_arg1="${#:$i:1}"
((i++))
extra_arg2="${#:$i:1}"
((i++))
# Print the output
echo "array1:"
print_one_array "${array1[#]}"
echo "array2:"
print_one_array "${array2[#]}"
echo "extra_arg1 = $extra_arg1"
echo "extra_arg2 = $extra_arg2"
echo "extra_args_array:"
print_one_array "${extra_args_array[#]}"
}
Example usage:
echo "Printing array1 and array2 plus some extra args"
print_two_arrays_plus_extra_args "${#array1[#]}" "${array1[#]}" \
"${#array2[#]}" "${array2[#]}" "hello" "world"
Example Output:
Printing array1 and array2 plus some extra args
array1:
one
two
three
array2:
four
five
six
seven
eight
extra_arg1 = hello
extra_arg2 = world
extra_args_array:
hello
world
References:
I referenced a lot of my own sample code from my eRCaGuy_hello_world repo here:
array_practice.sh
array_slicing_demo.sh
[my answer on bash array slicing] Unix & Linux: Bash: slice of positional parameters
An answer to my question on "How can I create and use a backup copy of all input args ("$#") in bash?" - very useful for general array manipulation of the input argument array
An answer to "How to pass array as an argument to a function in Bash", which confirmed to me this really important concept that:
You cannot pass an array, you can only pass its elements (i.e. the expanded array).
See also:
[another answer of mine on this topic] How to pass array as an argument to a function in Bash
Requirement: Function to find a string in an array.
This is a slight simplification of DevSolar's solution in that it uses the arguments passed rather than copying them.
myarray=('foobar' 'foxbat')
function isInArray() {
local item=$1
shift
for one in $#; do
if [ $one = $item ]; then
return 0 # found
fi
done
return 1 # not found
}
var='foobar'
if isInArray $var ${myarray[#]}; then
echo "$var found in array"
else
echo "$var not found in array"
fi
You can also create a json file with an array, and then parse that json file with jq
For example:
my-array.json:
{
"array": ["item1","item2"]
}
script.sh:
ARRAY=$(jq -r '."array"' $1 | tr -d '[],"')
And then call the script like:
script.sh ./path-to-json/my-array.json
You can find more ideas in this similar question: How to pass array as an argument to a function in Bash

Resources