Sunday, 1 January 2012

Some bash array functions

Following on from the previous post, here are some functions for bash arrays. I'll add to this post later with more functions.
Test if a variable is an array:
# is_array - Tests whether variable name is an array
# Parameters:
#   name - name of variable to test
# Returns:
#   0 if an array
#   1 otherwise
is_array()
{
    # Only takes one argument
    if [ "${#}" -ne "1" ]; then
        return 1
    fi

    # Name of the variable to test
    local name="$1"; shift
    # Run the declare command against the variable
    local x=$(declare -p $name 2>/dev/null)
    # If it errored then the name probably isn't even
    # a variable
    if [ "$?" -ne "0" ]; then
        return 1
    else
        # It is at least declared, test if it is an array
        if [ "${x:8:2}" == "-a" ]; then
            return 0
        else
            return 1
        fi
    fi
}
Push values onto an array:
# array_push - Pushes one or more values onto an array
# Parameters:
#   name - name of the destination array
#   ...  - values to push onto array
# Returns:
#   Nothing
array_push()
{
    # Need two or more arguments
    if [ "${#}" -lt "2" ]; then
        return
    fi

    # Name of the destination array
    local name="$1"; shift
    # Number of elements we're going to push
    local numElements=${#}
    # Counter
    local i=0
    # Starting position of destination array is the
    # size of the array
    eval local start=\$\{\#$name\[\@\]\}
    # Iterate over the elements supplied, adding them
    # to the destination array
    while [ "$i" -lt "$numElements" ]; do
        pos=$((start+i))
        eval $name\[$pos\]=\"$1\";
        shift
        ((i++))
    done
}
Pop from an array:
# array_pop - pops a value from the end of the array
# Parameters:
#   name - name of array
#   dest - optional name of variable to put value in
# Returns:
#   Nothing
array_pop()
{
    # Need at least one argument
    if [ "${#}" -eq "0" ]; then
        return
    fi

    # Name of the array
    local name="$1"; shift
    # If we have a second argument, use it to write value
    if [ -n "$1" ]; then
        local dest="$1"; shift
    else
        local dest=""
    fi
    # Size of the array
    eval local numElements=\$\{\#$name\[\@\]\}
    # Index of element is one less than size
    local index=$((numElements-1))
    # Get the value
    eval local value=\"\$\{$name\[$index\]\}\"
    if [ -n "$dest" ]; then
        # Update the specified dest variable
        eval $dest=\"$value\"
    else
        # Simply echo the value
        echo "$value"
    fi
    # Unset from the array
    eval unset $name\[$index\]
}
One thing to note is the clash between variable names. E.g. if you wanted to return the value into the name variable, it would be bad to call that variable 'value'. local doesn't help here, best option would probably to use obscure variable names that are unlikely to clash.
Walk an array:
# array_walk - Walks through an array, calling the
#              callback function for each element
# Parameters:
#   name     - name of array to walk
#   callback - name of callback function
#   ...      - any parameters to be passed to callback
# Returns:
#   Nothing
array_walk()
{
    # Name of array to walk over
    local name="$1"; shift
    # Name of call back function
    local callback="$1"; shift
    # TODO - We should probably check whether callback
    # is actually a function

    # Number of elements in array
    eval local numElements=\$\{\#$name\[\@\]\}
    # Counter
    local i=0
    # Iterate over the elements in the array
    while [ "$i" -lt "$numElements" ]; do
        # Get the value
        eval value=$\{$name\[$i\]\}
        # Check whether it is an array
        is_array $value
        if [ "$?" -eq "0" ]; then
            # If so, recursively walk that array
            array_walk "$value" "$callback" "$@"
        else
            # Just a variable, so call the callback
            # function giving it the args and value
            $callback "$@" "$value"
        fi
        ((i++))
    done
}
Now, a little play with those functions:
declare -a aTest

echo First push
array_push aTest 'Hello' 'there' 'you' ':)'
echo new array is ${aTest[@]}
echo

declare -a aTest2
echo Second push
array_push aTest2 'How' 'are' 'you today?'
echo second array is ${aTest2[@]}
echo

echo Third push to add second array to the first
array_push aTest 'aTest2'
echo first array is ${aTest[@]}
echo

echo Defining destination array
declare -a aMerged
echo defining callback function
merge()
{
    local name="$1"
    local value="$2"
    # Simple push the value onto the destination
    array_push "$name" "$value"
}
echo Doing walk
array_walk aTest merge aMerged
echo merged array is ${aMerged[@]}
echo

echo Popping last element of merged array by reference
array_pop aMerged val
echo Value is $val
echo and again
array_pop aMerged val
echo Value is $val
echo merged array is now ${aMerged[@]}
Running it gives us:
$ ./arrays2.sh 
First push
new array is Hello there you :)

Second push
second array is How are you today?

Third push to add second array to the first
first array is Hello there you :) aTest2

Defining destination array
defining callback function
Doing walk
merged array is Hello there you :) How are you today?

Popping last element of merged array by reference
Value is you today?
and again
Value is are
merged array is now Hello there you :) How
$
So that sort of works. Next I should add an array_indexes() and array_values(). Oh, and happy new year :D
Edit:
Added array_pop function.

5 comments:

  1. Nooo is_array returning 0 for true and 1 for false! Bloody sysadmin programmers /rollseyes

    ReplyDelete
  2. Oh and happy new year from NZ :)

    ReplyDelete
  3. Hah :) Happy new year to you too! And yeah, sorry about the whole 0/1 thing :D

    ReplyDelete
  4. he is actually correct with the 0/1 thing as 0 is defined as true in shell, everything else is false

    ReplyDelete
  5. Yeah, that's just JC being a developer :)

    ReplyDelete