How to slice an array in Bash

Viewed 151516

Looking the "Array" section in the bash(1) man page, I didn't find a way to slice an array.

So I came up with this overly complicated function:

#!/bin/bash

# @brief: slice a bash array
# @arg1:  output-name
# @arg2:  input-name
# @args:  seq args
# ----------------------------------------------
function slice() {
   local output=$1
   local input=$2
   shift 2
   local indexes=$(seq $*)

   local -i i
   local tmp=$(for i in $indexes 
                 do echo "$(eval echo \"\${$input[$i]}\")" 
               done)

   local IFS=$'\n'
   eval $output="( \$tmp )"
}

Used like this:

$ A=( foo bar "a  b c" 42 )
$ slice B A 1 2
$ echo "${B[0]}"  # bar
$ echo "${B[1]}"  # a  b c

Is there a better way to do this?

4 Answers

At the risk of beating a dead horse, I was inspired by @jandob's answer and made this version that

  1. Is simpler (doesn't have so much shift logic or rewriting of variables as often).
  2. Respects quoted strings without dealing with IFS (-r mode only).
  3. Allows the user to specify [start, end) slicing or [start, length] slicing via -l flag.
  4. Allows you to echo the resulting array (default behavior), or "return" it into a new array for use in the calling parent (via -r slicedArray).

Note: namerefs are only supported in Bash >= 4.3. To support earlier versions of Bash (i.e. Mac without Brew's bash), you'll need to use indirection instead: use a temp var to access array parameters, e.g. declare arrValuesCmd="$1[@]"; declare arr=("${!arrValuesCmd}"), and use eval for return values, e.g. eval $retArrName='("${newArr[@]}")' (note the single quotes around the array declaration).

array.slice() {
    # array.slice [-l] [-r returnArrayName] myArray 3 5
    # Default functionality is to use second number as end index for slice (exclusive).
    # Can instead use second number as length by passing `-l` flag.
    # `echo` doesn't maintain quoted entries, so pass in `-r returnArrayName` to keep them.
    declare isLength
    declare retArrName
    declare OPTIND=1

    while getopts "lr:" opt; do
        case "$opt" in
            l)
                # If `end` is slice length instead of end index
                isLength=true
                ;;
            r)
                retArrName="$OPTARG"
                ;;
        esac
    done

    shift $(( OPTIND - 1 ))

    declare -n arr="$1"
    declare start="$2"
    declare end="$3"
    declare arrLength="${#arr[@]}"

    declare newArr=()
    declare newArrLength

    # Bash native slicing:
    #   Positive index values: ${array:start:length}
    #   Negative index values: ${array: start: length}
    # To use negative values, a space is required between `:` and the variable
    #   because `${var:-3}` actually represents a default value,
    #   e.g. `myVar=${otherVal:-7}` represents (pseudo-code) `myVar=otherVal || myVar=7`
    if [[ -z "$end" ]]; then
        # If no end is specified (regardless of `-l`/length or index), default to the rest of the array
        newArrLength="$arrLength"
    elif [[ -n "$isLength" ]]; then
        # If specifying length instead of end-index, use native bash array slicing
        newArrLength="$(( end ))"
    else
        # If specifying end-index, use custom slicing based on a range of [start, end):
        newArrLength="$(( end - start ))"
    fi

    newArr=("${arr[@]: start: newArrLength}")

    if [[ -n "$retArrName" ]]; then
        declare -n retArr="$retArrName"
        retArr=("${newArr[@]}")
    else
        echo "${newArr[@]}"
    fi
}

Examples:

myArray=(x y 'a b c' z 5 14)   # length=6

array.slice myArray 2 4
# > a b c z


array.slice -l myArray 3 2
# > z 5



# Note: Output was manually quoted to show the result more clearly.
# Actual stdout content won't contain those quotes, which is
#   why the `-r returnArray` option was added.

array.slice -r slicedArray myArray -5 -3   # equivalent of [2, 4)
# > (null)
echo -e "myArray (length=${#myArray[@]}): ${myArray[@]} \nslicedArray (length=${#slicedArray[@]}): ${slicedArray[@]}"
# > myArray (length=6): x y 'a b c' z 5 14 
# > slicedArray (length=2): 'a b c' z


array.slice -lr slicedArray myArray -5 3   # length instead of index, equivalent of [2, 5)
# > (null)
echo -e "myArray (length=${#myArray[@]}): ${myArray[@]} \nslicedArray (length=${#slicedArray[@]}): ${slicedArray[@]}"
# > myArray (length=6): x y 'a b c' z 5 14 
# > slicedArray (length=3): 'a b c' z 5
Related