Arrays in BASH

Arrays in BASH

Created:12 Feb 2017 14:48:27 , in  Host development

Once one is somewhat familiar with basic number and string handling rules in BASH, it is time to move on to learning about a container for these. BASH provides one such a convenient container in the form of array.

This article is about the number indexed arrays. If you've met the concept somewhere else before you should have no problem learning basic rules here now. After all, arrays in BASH work in a very similar manner to how they work in other programming languages, what differs is syntax. For these of you who haven't dealt with arrays so far there is an introductory section on the topic below.

This article has two parts about it. First of them is above-mentioned introductory section, the other a fairly large BASH script, which utilizes arrays extensively. The code the script consists of has been splat into a few smaller chunks. Each of them is followed by an explanatory comment.

BASH array operations and syntax

Suppose you have four strings: a b c d, and you want to place them all in array, here is how you could achieve this:


arr=(a b c d) 

To take a look at what is in your array you use declare built-in:


declare -p arr

If you need to get all the values stored in array, you can do that in two ways:


${arr[@]} or ${arr[*]}

You can obtain all the keys in array:


${!arr[@]}

Frequently the number of items in the array is needed, here is the syntax for it:


${#arr[@]}

Obtaining particular value from array can be done as follows:


${arr[0]}

Using 0 as index results in first value being returned.

Like in other programming languages, arrays in BASH are zero-based.

To add another item to the end of array you use syntax:


arr+=("new item")

Removing array values is done using unset built-n, you also need an index of the value you want to remove:


unset arr[3]

You can loop over array values like this:


for item in "${arr[@]}"; do echo $item; done;

You can also loop over values starting from the last one:


for (( i=${#arr[@]};i>0; i-- )); do echo "${arr[$i - 1]}";done

It is possible to join all items in array with a separator:


str=$(IFS=,; echo "${arr[*]}")

To break up a string and place resulting parts in array you use IFS variable:


IFS=/ read -a arr <<< "a/b/c/d/e"

IFS stores separator(s) to be used for splitting, in the above case it is "/"

One can obtain a slice of the original array using parameter expansions.


arr_slice=("${arr[@]:1:2}")

Parameter expansions can also be used to update all elements of array in one go. For example, the line of code below adds + sign to the beginning of each string stored in array arr.


arr=( "${arr[@]/#/+}" )

Values in array can be updated in various other ways using parameter expansions. Check BASH manual for more information on this.

BASH array operations and syntax table

Array operation Syntax
Creating array

arr=(a b c d)
Inspecting

declare -p arr
Array values

${arr[@]} or ${arr[*]}
Array keys

${!arr[@]}
Number of items

${#arr[@]}
Single value

${arr[index]}
Adding new value

arr+=("new item")
Removing a value

unset arr[index]
Looping using for loop

for item in "${arr[@]}"; do echo $item; done;
Looping from the end

for (( i=${#arr[@]};i>0; i-- )); do echo "${arr[$i - 1]}";done
Joining items in array with ,

str=$(IFS=,; echo "${arr[*]}")
Breaking up string

IFS=/ read -a arr <<< "a/b/c/d/e"
Slicing

("${arr[@]:1:2}")
Adding prefix to each value

( "${arr[@]/#/some}" )

Next version name of computer program

Imagine you have written a computer program, possibly a website, and you store its consecutive versions in a directory. Each version of the program has its own sub-directory with unique name in this directory. The version sub-directories could be named as follows: 0.1, 0.1.1, 0.5, 1.0, 1.2.1 etc. .

At some point you come to the conclusion you no longer want to deal with these version names by hand. Instead you would like a program that simply gives the next version name if provided no extra instruction (e.g next version of 1.1 would be 1.2) or finds correct version when clued (e.g. you might need version 1.3.4 to be 1.4.0 ).

Here is a program that finds next version name, it is appropriately called find_next_version_name.sh. It consists of a few global variables and 4 functions (each function has a short description above its name). The find_next_version_name.sh makes extensive use of arrays.

find_next_version_name.sh - part 1


#!/usr/bin/env bash

# Program name: find_next_version_name.sh
# Author: Sylwester Wojnowski
# WWW: wojnowski.net.pl

#directory in which consecutive version sub-directories are stored
VERSIONS_DIR=/tmp/my_program/versions/
# normalize version directory names. 
#If set to 1, all version directories will be renamed to have the same length
NORMALIZE=1

########## Private ############ 

# set by find versions
VERSIONS=()
# set by find_latest_version
LAST_VERSION=

# find_versions() finds directories with names like 0.45, 1.1 1.11.5 or 1.1.8.9 in VERSIONS_DIR 
# and places them in global VERSIONS array 
find_versions(){
  declare -n ITEMS=VERSIONS
  
  local REGEX='^([0-9]+\.)+[0-9]+$'
                  
  local ASSORTED=
  local VERSIONS_DIR="$1"

  # make sure some directory is given 
  [[ -z "$VERSIONS_DIR" ]] && { 
    echo "Versions directory not given."
    exit 1
  }

  # check, you can enter 
  # suppress output from command cd
  cd "$VERSIONS_DIR" &> /dev/null

  # check status of the last command
  [[ "$?" != "0" ]] && {
    echo "Versions directory does not exist. Exiting ..."
    exit 1
  }

  # store all directory names in an array
  ASSORTED=( * )     

  # filter version directories out from the rest and store them in ITEMS array
  for item in "${ASSORTED[@]}"; do 
    [[ $item =~ $REGEX ]] && {
      ITEMS+=("$item")
    }
  done

  # exit if no version found
  (( ${#ITEMS[@]} == 0 )) && {
    printf "Directory $VERSIONS_DIR hosts no versions at the moment. Add first, perhaps something like 0.0.1 ..."
    exit;
  }

  # get back to the previous working directory
  cd "$OLDPWD"
  return
}

Comments on the syntax used in the above piece of code

Create new empty VERSIONS array:


VERSIONS=()

Display values in array VERSIONS (see my article on declare and env for more details):


declare -p VERSIONS

Array ITEMS references global array versions. (see my article on env an bash declare for more details):


declare -n ITEMS=VERSIONS

Populate array ASSORTED with files and directories from the current working directory:


ASSORTED=( * )

Reference all items ( @ ) of ASSORTED at the same time:


${ASSORTED[@]}

If you need to reference a single item from an array you would use a numeric value in place of @:


${ASSORTED[1]}

Iterate through array ASSORTED and assign items that pass regular expressions based test to array ITEMS.


for item in "${ASSORTED[@]}"; do 
    [[ $item =~ $REGEX ]] && {
      ITEMS+=("$item")
    }
done

Find number of items currently in array ITEMS:


${#ITEMS[@]}

find_next_version_name.sh part 2


# standardize_versions() updates VERSIONS array names in such a way that each version has length of the longest version name
# e.g. if the longest version name is 1.1.1.1 and current version is 1.1, 1.1 becomes 1.1.0.0
# Original sub-directory names remain the same unless NORMALIZE global variable has value 1.
standardize_versions(){
  local LONGEST=0
  
  # find longest version name
  for version in "${VERSIONS[@]}"; do
    local vlen=
    IFS=. read -r -a vlen <<< "$version" 
    (( ${#vlen[@]} > $LONGEST  )) && {
      LONGEST=${#vlen[@]};
    }  
  done
  
  # extend shorter version names with '.0'
  for (( i=0 ; $i < ${#VERSIONS[@]}; i++ )); do
    local orig_version=${VERSIONS[$i]} 
    local version=${VERSIONS[$i]}
    vlen=
    IFS=. read -r -a vlen <<< "$version"
    while (( ${#vlen[@]} < $LONGEST )); do
      version="${version}.0"
      IFS=. read -r -a vlen <<< "$version"
    done 
    VERSIONS[$i]="$version"
    
    (( $NORMALIZE == 1 )) && [[ "$version" != "$orig_version" ]] && {
      cd "$VERSIONS_DIR" 
      mv "$orig_version" "$version"
      cd "$OLDPWD" 
    }
  done
}

Comments on the syntax used in the above piece of code:

Split variable $version (string) using dot and place resulting values in array vlen:


IFS=. read -r -a vlen <<< "$version"

Iterate over array VERSIONS using for loop:


for (( i=0 ; $i < ${#VERSIONS[@]}; i++ )); do
.
.
.
done

Set variable orig_version to value ${VERSIONS[$i]}, where VERSIONS is an array and $i is a number ( holds particular array index )


orig_version=${VERSIONS[$i]}

find_next_version_name.sh part 3


# find_last_version() finds the highest version ( last created might not be the most recent )
# 1.1.0.1 is higher than 1.1.0.0 and 1.2.0.0 is higher 1.1.20.1
find_last_version(){

  declare -n last_version=LAST_VERSION
  local last_version_a=
  local next_version_a=
  for version in "${VERSIONS[@]}"; do
    # set first version name
    [[ -z "$last_version" ]] && {
      last_version="$version"
      IFS=. read -r -a last_version_a <<< "$version"  
    #  compare version names here  
    } || {
      IFS=. read -r -a next_version_a <<< "$version"
      # now compare two arrays
      for (( i=0; $i < ${#last_version_a[@]}; i++ )); do
        (( ${last_version_a[$i]} > ${next_version_a[$i]} )) && { break; }
        (( ${last_version_a[$i]} < ${next_version_a[$i]} )) && { 
          last_version=$version;
          IFS=. read -r -a last_version_a <<< "$version"
          break;
        }  
        (( ${last_version_a[$i]} == ${next_version_a[$i]} )) && {
          continue;
        }   
      done
    }
  done
  return  
}

Compare numeric values ${last_version_a[$i]} and ${next_version_a[$i]}. Last_version_a and next_version_a are arrays and ${last_version_a[$i]} and # ${next_version_a[$i]} are values with index $i (number)

  
(( ${last_version_a[$i]} > ${next_version_a[$i]} ))
(( ${last_version_a[$i]} < ${next_version_a[$i]} ))
(( ${last_version_a[$i]} == ${next_version_a[$i]} ))

find_next_version_name part 4


# find_next_version_name() generates next version name.
# If invoked with no argument, it updates the rightmost part of the highest version name
# ( e.g. 1.2.1 becomes 1.2.2 ).
# if invoked with index, the function increments number at the index given as its sole argument by 1;
# e.g. if the highest version is 1.2.2 and the function is invokes like this: find_next_version_name 1 
# next version will be 1.3.0, invoking it as follows: find_next_version_name 0 produces 2.0.0 

find_next_version_name() {

  find_versions "$VERSIONS_DIR"
  standardize_versions
  find_last_version
  local last_version=
  local index=-1
  local indices=
 
  IFS=. read -r -a last_version <<< "$LAST_VERSION"
  [[ ! -z "$1" ]] && {
    # check if given index exists
    indices="${!last_version[@]}" 
    for i in $indices; do
      [[ "$1" == "$i" ]] && {
        index="$i"
        break; 
      }
    done
     
    (( index == -1 )) && {
      printf "*** \n ${1} is not a valid index. Possible indices are 0 numbered. Each index correspond to a placement of a number in dot separated string representing the last version, which in this particular case is ${LAST_VERSION}. Hence, possible values here are ${indices}. \n Exiting ...\n *** \n"
      exit 1  
    }
      
    (( last_version[$index]+=1 )) 

    for (( (( index++ )), j=index; $j < ${#last_version[@]}; j++  )); do 
      [[ last_version[$j] ]] && { last_version[$j]=0 ;}
    done
  } || {
    (( last_version[$index]+=1 ))
  }
  echo $( IFS=.; echo "${last_version[*]}")
}

#run the program
find_next_version_name $1

Comments on the syntax used in the above piece of code:

Display indices of array last_version:


${!last_version[@]}

Increment value of array last_version with index $index:


(( last_version[$index]+=1 ))

Test for existence of index $i in array last_version:


[[ last_version[$j] ]]

Display all values currently in array last_version ( * works the same as @ )


${last_version[*]}

Join all items in array last_version using dot symbol (.)


echo $( IFS=.; echo "${last_version[*]}")

Other things worth knowing about BASH arrays

There is special syntax for both removing indices from an array and destroying whole arrays. In both cases built-in unset is used:

Remove member variable at index 0:


unset ${ARRAY[0]}

Destroy array ARRAY:


unset ARRAY

Running the program

If you want to run the program used as an example above, save all its four pieces to a file with the name find_next_version_name.sh, and make the file executable:


$ chmod 755 find_next_version_name.sh

Next update VERSIONS_DIR variable value at the beginning of the file to point to a directory (trailing slash required) you store you program sub-directories in. The sub-directories must have names that follow the pattern number.number. ... .number for the program to find them.

Run the program:


$ find_next_version_name.sh

Conclusion

Arrays in BASH, in terms how they behave and what they offer, are not that different from what one normally expects and gets when programming computers in a modern programming language. However, array syntax BASH provides looks fairly cryptic and different in many cases, at least at the first sight. Even more so when a piece of code is based largely on arrays. Learning the syntax requires a little bit of open mind, time, attention to detail and some decent examples resource. I hope this article will become such a resource to you.

This post was updated on 04 Nov 2017 14:28:19

Tags:  BASH 


Author, Copyright and citation

Author

Sylwester Wojnowski

Author of the above article, Sylwester Wojnowski, is sWWW admin and owner.He enjoys doing Maths and studying algorithms, writing code in scripting and command languages, Thrash Metal music and playing electric guitar.

Copyrights

©Copyright, 2018 Sylwester Wojnowski. This article may not be reproduced or published as a whole or in parts without permission from the author. If you share it, please give author credit and do not remove embedded links.

Computer code, if present in the article, is excluded from the above and licensed under GPLv3.

Citation

Cite this article as:

Wojnowski, Sylwester. "Arrays in BASH." From sWWW - Code For The Web . https://wojnowski.net.pl//main/index/arrays-in-bash

Post navigation

Previous:
  BASH recursion examples - part 2

Next:
  installing PHPUnit