3.24. Arrays

Newer versions of bash support one-dimensional arrays. Arrays may be declared with the variable[xx] notation or explicitly by a declare -a variable statement. To dereference (find the contents of) an array variable, use curly bracket notation, that is, ${variable[xx]}.


Example 3-149. Simple array usage

   1 #!/bin/bash
   2 
   3 
   4 area[11]=23
   5 area[13]=37
   6 area[51]=UFOs
   7 
   8 # Note that array members need not be consecutive
   9 # or contiguous.
  10 
  11 # Some members of the array can be left uninitialized.
  12 # Gaps in the array are o.k.
  13 
  14 
  15 echo -n "area[11] = "
  16 echo ${area[11]}
  17 echo -n "area[13] = "
  18 echo ${area[13]}
  19 # Note that {curly brackets} needed
  20 echo "Contents of area[51] are ${area[51]}."
  21 
  22 # Contents of uninitialized array variable print blank.
  23 echo -n "area[43] = "
  24 echo ${area[43]}
  25 echo "(area[43] unassigned)"
  26 
  27 echo
  28 
  29 # Sum of two array variables assigned to third
  30 area[5]=`expr ${area[11]} + ${area[13]}`
  31 echo "area[5] = area[11] + area[13]"
  32 echo -n "area[5] = "
  33 echo ${area[5]}
  34 
  35 area[6]=`expr ${area[11]} + ${area[51]}`
  36 echo "area[6] = area[11] + area[51]"
  37 echo -n "area[6] = "
  38 echo ${area[6]}
  39 # This doesn't work because
  40 # adding an integer to a string is not permitted.
  41 
  42 echo
  43 echo
  44 echo
  45 
  46 # -----------------------------------------------------------------
  47 # Another array, "area2".
  48 # Another way of assigning array variables...
  49 # array_name=( XXX YYY ZZZ ... )
  50 
  51 area2=( zero one two three four )
  52 
  53 echo -n "area2[0] = "
  54 echo ${area2[0]}
  55 # Aha, zero-based indexing (first element of array is [0], not [1]).
  56 
  57 echo -n "area2[1] = "
  58 echo ${area2[1]}  # [1] is second element of array.
  59 # -----------------------------------------------------------------
  60 
  61 
  62 echo
  63 echo
  64 echo
  65 
  66 # -----------------------------------------------
  67 # Yet another array, "area3".
  68 # Yet another way of assigning array variables...
  69 # array_name=([xx]=XXX [yy]=YYY ...)
  70 
  71 area3=([17]=seventeen [24]=twenty-four)
  72 
  73 echo -n "area3[17] = "
  74 echo ${area3[17]}
  75 
  76 echo -n "area3[24] = "
  77 echo ${area3[24]}
  78 # -----------------------------------------------
  79 
  80 
  81 exit 0

Arrays variables have a syntax all their own, and even standard Bash operators have special options adapted for array use.


Example 3-150. Some special properties of arrays

   1 #!/bin/bash
   2 
   3 declare -a colors
   4 # Permits declaring an array without specifying size.
   5 
   6 echo "Enter your favorite colors (separated from each other by a space)."
   7 
   8 read -a colors
   9 # Special option to 'read' command,
  10 # allowing it to assign elements in an array.
  11 
  12 echo
  13 
  14   element_count=${#colors[@]} # Special syntax to extract number of elements in array.
  15 # element_count=${#colors[*]} works also.
  16 #
  17 # The "@" variable allows word splitting within quotes
  18 # (extracts variables separated by whitespace).
  19 index=0
  20 
  21 # List all the elements in the array.
  22 while [ "$index" -lt "$element_count" ]
  23 do
  24   echo ${colors[$index]}
  25   let "index = $index + 1"
  26 done
  27 # Each array element listed on a separate line.
  28 # If this is not desired, use  echo -n "${colors[$index]} "
  29 #
  30 # Doing it with a "for" loop instead:
  31 #   for i in "${colors[@]}"
  32 #   do echo "$i"
  33 #   done
  34 # (Thanks, S.C.)
  35 
  36 echo
  37 
  38 # Again, list all the elements in the array, but using a more elegant method.
  39   echo ${colors[@]}
  40 # echo ${colors[*]} works also.
  41 
  42 
  43 echo
  44 
  45 exit 0

As seen in the previous example, either ${array_name[@]} or ${array_name[*]} refers to all the elements of the array. Similarly, to get a count of the number of elements in an array, use either ${#array_name[@]} or ${#array_name[*]}. ${#array_name} is the length (number of characters) of ${array_name[0]}, the first element of the array.


Example 3-151. Of empty arrays and empty elements

   1 #!/bin/bash
   2 # empty-array.sh
   3 
   4 # An empty array is not the same as an array with empty elements.
   5 
   6 array0=( first second third )
   7 array1=( '' )   # "array1" has one empty element.
   8 array2=( )      # No elements... "array2" is empty.
   9 
  10 echo
  11 
  12 echo "Elements in array0:  ${array0[@]}"
  13 echo "Elements in array1:  ${array1[@]}"
  14 echo "Elements in array2:  ${array2[@]}"
  15 echo
  16 echo "Length of first element in array0 = ${#array0}"
  17 echo "Length of first element in array1 = ${#array1}"
  18 echo "Length of first element in array2 = ${#array2}"
  19 echo
  20 echo "Number of elements in array0 = ${#array0[*]}"  # 3
  21 echo "Number of elements in array1 = ${#array1[*]}"  # 1  (surprise!)
  22 echo "Number of elements in array2 = ${#array2[*]}"  # 0
  23 
  24 echo
  25 
  26 # Thanks, S.C.
  27 
  28 exit 0

The relationship of ${array_name[@]} and ${array_name[*]} is analogous to that between $@ and $*. This powerful array notation has a number of uses.

   1 # Copying an array.
   2 array2=( "${array1[@]}" )
   3 
   4 # Adding an element to an array.
   5 array=( "${array[@]}" "new element" )
   6 # or
   7 array[${#array[*]}]="new element"
   8 
   9 # Thanks, S.C.

--

Arrays permit deploying old familiar algorithms as shell scripts. Whether this is necessarily a good idea is left to the reader to decide.


Example 3-152. An old friend: The Bubble Sort

   1 #!/bin/bash
   2 
   3 # Bubble sort, of sorts.
   4 
   5 # Recall the algorithm for a bubble sort. In this particular version...
   6 
   7 # With each successive pass through the array to be sorted,
   8 # compare two adjacent elements, and swap them if out of order.
   9 # At the end of the first pass, the "heaviest" element has sunk to bottom.
  10 # At the end of the second pass, the next "heaviest" one has sunk next to bottom.
  11 # And so forth.
  12 # This means that each successive pass needs to traverse less of the array.
  13 # You will therefore notice a speeding up in the printing of the later passes.
  14 
  15 
  16 exchange()
  17 {
  18   # Swaps two members of the array.
  19   local temp=${Countries[$1]} # Temporary storage for element getting swapped out.
  20   Countries[$1]=${Countries[$2]}
  21   Countries[$2]=$temp
  22   
  23   return
  24 }  
  25 
  26 declare -a Countries  # Declare array, optional here since it's initialized below.
  27 
  28 Countries=(Netherlands Ukraine Zaire Turkey Russia Yemen Syria Brazil Argentina Nicaragua Japan Mexico Venezuela Greece England Israel Peru Canada Oman Denmark Wales France Kenya Qatar Liechtenstein Hungary)
  29 # Couldn't think of one starting with X (darn).
  30 
  31 clear  # Clear the screen to start with. 
  32 
  33 echo "0: ${Countries[*]}"  # List entire array at pass 0.
  34 
  35 number_of_elements=${#Countries[@]}
  36 let "comparisons = $number_of_elements - 1"
  37 
  38 count=1 # Pass number.
  39 
  40 while [ $comparisons -gt 0 ]   # Beginning of outer loop
  41 do
  42 
  43   index=0  # Reset index to start of array after each pass.
  44 
  45   while [ $index -lt $comparisons ] # Beginning of inner loop
  46   do
  47     if [ ${Countries[$index]} \> ${Countries[`expr $index + 1`]} ]
  48     # If out of order...
  49     # Recalling that \> is ASCII comparison operator.
  50     then
  51       exchange $index `expr $index + 1`  # Swap.
  52     fi  
  53     let "index += 1"
  54   done # End of inner loop
  55   
  56 
  57 let "comparisons -= 1"
  58 # Since "heaviest" element bubbles to bottom, we need do one less comparison each pass.
  59 
  60 echo
  61 echo "$count: ${Countries[@]}"
  62 # Print resultant array at end of each pass.
  63 echo
  64 let "count += 1"   # Increment pass count.
  65 
  66 done  # End of outer loop
  67 
  68 # All done.
  69 
  70 exit 0

--

Arrays enable implementing a shell script version of the Sieve of Erastosthenes. Of course, a resource-intensive application of this nature should really be written in a compiled language, such as C. It runs excruciatingly slowly as a script.


Example 3-153. Complex array application: Sieve of Erastosthenes

   1 #!/bin/bash
   2 
   3 # sieve.sh
   4 # Sieve of Erastosthenes
   5 # Ancient algorithm for finding prime numbers.
   6 
   7 # This runs a couple of orders of magnitude
   8 # slower than equivalent C program.
   9 
  10 LOWER_LIMIT=1
  11 # Starting with 1.
  12 UPPER_LIMIT=1000
  13 # Up to 1000.
  14 # (You may set this higher...
  15 #  if you have time on your hands.)
  16 
  17 PRIME=1
  18 NON_PRIME=0
  19 
  20 let SPLIT=UPPER_LIMIT/2
  21 # Optimization:
  22 # Need to test numbers only
  23 # halfway to upper limit.
  24 
  25 
  26 declare -a Primes
  27 # Primes[] is an array.
  28 
  29 
  30 initialize ()
  31 {
  32 # Initialize the array.
  33 
  34 i=$LOWER_LIMIT
  35 until [ "$i" -gt "$UPPER_LIMIT" ]
  36 do
  37   Primes[i]=$PRIME
  38   let "i += 1"
  39 done
  40 # Assume all array members guilty (prime)
  41 # until proven innocent.
  42 }
  43 
  44 print_primes ()
  45 {
  46 # Print out the members of the Primes[] array
  47 # tagged as prime.
  48 
  49 i=$LOWER_LIMIT
  50 
  51 until [ "$i" -gt "$UPPER_LIMIT" ]
  52 do
  53 
  54   if [ "${Primes[i]}" -eq "$PRIME" ]
  55   then
  56     printf "%8d" $i
  57     # 8 spaces per number
  58     # gives nice, even columns.
  59   fi
  60   
  61   let "i += 1"
  62   
  63 done
  64 
  65 }
  66 
  67 sift ()
  68 {
  69 # Sift out the non-primes.
  70 
  71 let i=$LOWER_LIMIT+1
  72 # We know 1 is prime, so
  73 # let's start with 2.
  74 
  75 until [ "$i" -gt "$UPPER_LIMIT" ]
  76 do
  77 
  78 if [ "${Primes[i]}" -eq "$PRIME" ]
  79 # Don't bother sieving numbers
  80 # already sieved (tagged as non-prime).
  81 then
  82 
  83   t=$i
  84 
  85   while [ "$t" -le "$UPPER_LIMIT" ]
  86   do
  87     let "t += $i "
  88     Primes[t]=$NON_PRIME
  89     # Tag as non-prime
  90     # all multiples.
  91   done
  92 
  93 fi  
  94 
  95   let "i += 1"
  96 done  
  97 
  98 
  99 }
 100 
 101 
 102 # Invoke the functions sequentially.
 103 initialize
 104 sift
 105 print_primes
 106 echo
 107 # This is what they call structured programming.
 108 
 109 exit 0
 110 
 111 
 112 
 113 # ----------------------------------------------- #
 114 # Code below line will not execute.
 115 
 116 # This improved version of the Sieve, by Stephane Chazelas,
 117 # executes somewhat faster.
 118 
 119 # Must invoke with command-line argument (limit of primes).
 120 
 121 UPPER_LIMIT=$1           # From command line.
 122 let SPLIT=UPPER_LIMIT/2  # Halfway to max number.
 123 
 124 Primes=( '' $(seq $UPPER_LIMIT) )
 125 
 126 i=1
 127 until (( ( i += 1 ) > SPLIT ))  # Need check only halfway.
 128 do
 129   if [[ -n $Primes[i] ]]
 130   then
 131     t=$i
 132     until (( ( t += i ) > UPPER_LIMIT ))
 133     do
 134       Primes[t]=
 135     done
 136   fi  
 137 done  
 138 echo ${Primes[*]}

Compare this array-based prime number generator with with an alternative that does not use arrays, Example A-10.

--

Fancy manipulation of array "subscripts" may require intermediate variables. For projects involving this, again consider using a more powerful programming language, such as Perl or C.


Example 3-154. Complex array application: Exploring a weird mathematical series

   1 #!/bin/bash
   2 
   3 # Douglas Hofstadter's notorious "Q-series":
   4 
   5 # Q(1) = Q(2) = 1
   6 # Q(n) = Q(n - Q(n-1)) + Q(n - Q(n-2)), for n>2
   7 
   8 # This is a "chaotic" integer series with strange and unpredictable behavior.
   9 # The first 20 terms of the series are:
  10 # 1 1 2 3 3 4 5 5 6 6 6 8 8 8 10 9 10 11 11 12 
  11 
  12 # See Hofstadter's book, "Goedel, Escher, Bach: An Eternal Golden Braid", p. 137, ff.
  13 
  14 
  15 LIMIT=100     # Number of terms to calculate
  16 LINEWIDTH=20  # Number of terms printed per line
  17 
  18 Q[1]=1  # First two terms of series are 1.
  19 Q[2]=1
  20 
  21 echo
  22 echo "Q-series [$LIMIT terms]:"
  23 echo -n "${Q[1]} "   # Output first two terms.
  24 echo -n "${Q[2]} "
  25 
  26 for ((n=3; n <= $LIMIT; n++))  # C-like loop conditions.
  27 do   # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]]  for n>2
  28 # Need to break the expression into intermediate terms,
  29 # since Bash doesn't handle complex array arithmetic very well.
  30 
  31   let "n1 = $n - 1"  # n-1
  32   let "n2 = $n - 2"  # n-2
  33   
  34   t0=`expr $n - ${Q[n1]}`  # n - Q[n-1]
  35   t1=`expr $n - ${Q[n2]}`  # n - Q[n-2]
  36   
  37   T0=${Q[t0]}   # Q[n - Q[n-1]]
  38   T1=${Q[t1]}   # Q[n - Q[n-2]]
  39 
  40 Q[n]=`expr $T0 + $T1`   # Q[n - Q[n-1]] + Q[n - ![n-2]]
  41 echo -n "${Q[n]} "
  42 
  43 if [ `expr $n % $LINEWIDTH` -eq 0 ]    # Format output.
  44 then   #     mod
  45   echo # Break lines into neat chunks.
  46 fi
  47 
  48 done
  49 
  50 echo
  51 
  52 exit 0
  53 
  54 # This is an iterative implementation of the Q-series.
  55 # The more intuitive recursive implementation is left as an exercise for the reader.
  56 # Warning: calculating this series recursively takes a *very* long time.

--

Bash supports only one-dimensional arrays, however a little trickery permits simulating multi-dimensional ones.


Example 3-155. Simulating a two-dimensional array, then tilting it

   1 #!/bin/bash
   2 # Simulating a two-dimensional array.
   3 
   4 # A two-dimensional array stores rows sequentially.
   5 
   6 Rows=5
   7 Columns=5
   8 
   9 declare -a alpha   # char alpha [Rows] [Columns];
  10 # Unnecessary declaration.
  11 
  12 load_alpha ()
  13 {
  14 local rc=0
  15 local index
  16 
  17 
  18 for i in A B C D E F G H I J K L M N O P Q R S T U V W X Y
  19 do
  20   local row=`expr $rc / $Columns`
  21   local column=`expr $rc % $Rows`
  22   let "index = $row * $Rows + $column"
  23   alpha[$index]=$i              # alpha[$row][$column]
  24   let "rc += 1"
  25 done  
  26 
  27 # Simpler would be
  28 #   declare -a alpha=( A B C D E F G H I J K L M N O P Q R S T U V W X Y )
  29 # but this somehow lacks the "flavor" of a two-dimensional array.
  30 }
  31 
  32 print_alpha ()
  33 {
  34 local row=0
  35 local index
  36 
  37 echo
  38 
  39 while [ "$row" -lt "$Rows" ]   # Print out in "row major" order -
  40 do                             # columns vary while row (outer loop) remains the same.
  41 
  42   local column=0
  43   
  44   while [ "$column" -lt "$Columns" ]
  45   do
  46     let "index = $row * $Rows + $column"
  47     echo -n "${alpha[index]} "  # alpha[$row][$column]
  48     let "column += 1"
  49   done
  50 
  51   let "row += 1"
  52   echo
  53 
  54 done  
  55 
  56 # The simpler equivalent is
  57 #   echo ${alpha[*]} | xargs -n $Columns
  58 
  59 echo
  60 }
  61 
  62 filter ()  # Filter out negative array indices.
  63 {
  64 
  65 echo -n "  "  # Provides the tilt.
  66 
  67 if [[ "$1" -ge 0 &&  "$1" -lt "$Rows" && "$2" -ge 0 && "$2" -lt "$Columns" ]]
  68 then
  69     let "index = $1 * $Rows + $2"
  70     # Now, print it rotated.
  71     echo -n " ${alpha[index]}"  # alpha[$row][$column]
  72 fi    
  73 
  74 }
  75   
  76 
  77 
  78 
  79 rotate ()  # Rotate the array 45 degrees ("balance" it on its lower lefthand corner).
  80 {
  81 local row
  82 local column
  83 
  84 for (( row = Rows; row > -Rows; row-- ))  # Step through the array backwards.
  85 do
  86 
  87   for (( column = 0; column < Columns; column++ ))
  88   do
  89 
  90     if [ "$row" -ge 0 ]
  91     then
  92       let "t1 = $column - $row"
  93       let "t2 = $column"
  94     else
  95       let "t1 = $column"
  96       let "t2 = $column + $row"
  97     fi  
  98 
  99     filter $t1 $t2   # Filter out negative array indices.
 100   done
 101 
 102   echo; echo
 103 
 104 done 
 105 
 106 # Array rotation inspired by examples (pp. 143-146) in
 107 # "Advanced C Programming on the IBM PC", by Herbert Mayer
 108 # (see bibliography).
 109 
 110 }
 111 
 112 
 113 #-----------------------------------------------------#
 114 load_alpha     # Load the array.
 115 print_alpha    # Print it out.  
 116 rotate         # Rotate it 45 degrees counterclockwise.
 117 #-----------------------------------------------------#
 118 
 119 
 120 # This is a rather contrived, not to mention kludgy simulation.
 121 #
 122 # Exercise #1 for the reader:
 123 # Rewrite the array loading and printing functions
 124 # in a more intuitive and elegant fashion.
 125 #
 126 # Exercise #2:
 127 # Figure out how the array rotation functions work.
 128 # Hint: think about the implications of backwards-indexing an array.
 129 
 130 exit 0