如何在Bash中对数组进行切片

时间:2009-08-26 16:04:13

标签: arrays bash slice

在bash(1)手册页中查看“数组”部分,我没有找到切片数组的方法。

所以我想出了这个过于复杂的功能:

#!/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 )"
}

像这样使用:

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

有更好的方法吗?

5 个答案:

答案 0 :(得分:244)

请参阅Bash man页面中的Parameter Expansion部分。 A[@]返回数组的内容,:1:2从索引1开始,获取一长度为2的片段。

A=( foo bar "a  b c" 42 )
B=("${A[@]:1:2}")
C=("${A[@]:1}")       # slice to the end of the array
echo "${B[@]}"        # bar a  b c
echo "${B[1]}"        # a  b c
echo "${C[@]}"        # bar a  b c 42
echo "${C[@]: -2:2}"  # a  b c 42 # The space before the - is necesssary

请注意,“a b c”是一个数组元素(并且它包含一个额外的空格)这一事实被保留。

答案 1 :(得分:45)

还有一个方便的快捷方式,可以从指定的索引开始获取数组的所有元素。例如“$ {A [@]:1}”将是数组的“尾部”,即没有第一个元素的数组。

version=4.7.1
A=( ${version//\./ } )
echo "${A[@]}"    # 4 7 1
B=( "${A[@]:1}" )
echo "${B[@]}"    # 7 1

答案 2 :(得分:1)

像在Python中一样进行数组切片(来自rebash库):

array_slice() {
    local __doc__='
    Returns a slice of an array (similar to Python).

    From the Python documentation:
    One way to remember how slices work is to think of the indices as pointing
    between elements, with the left edge of the first character numbered 0.
    Then the right edge of the last element of an array of length n has
    index n, for example:
    ```
    +---+---+---+---+---+---+
    | 0 | 1 | 2 | 3 | 4 | 5 |
    +---+---+---+---+---+---+
    0   1   2   3   4   5   6
    -6  -5  -4  -3  -2  -1
    ```

    >>> local a=(0 1 2 3 4 5)
    >>> echo $(array.slice 1:-2 "${a[@]}")
    1 2 3
    >>> local a=(0 1 2 3 4 5)
    >>> echo $(array.slice 0:1 "${a[@]}")
    0
    >>> local a=(0 1 2 3 4 5)
    >>> [ -z "$(array.slice 1:1 "${a[@]}")" ] && echo empty
    empty
    >>> local a=(0 1 2 3 4 5)
    >>> [ -z "$(array.slice 2:1 "${a[@]}")" ] && echo empty
    empty
    >>> local a=(0 1 2 3 4 5)
    >>> [ -z "$(array.slice -2:-3 "${a[@]}")" ] && echo empty
    empty
    >>> [ -z "$(array.slice -2:-2 "${a[@]}")" ] && echo empty
    empty

    Slice indices have useful defaults; an omitted first index defaults to
    zero, an omitted second index defaults to the size of the string being
    sliced.
    >>> local a=(0 1 2 3 4 5)
    >>> # from the beginning to position 2 (excluded)
    >>> echo $(array.slice 0:2 "${a[@]}")
    >>> echo $(array.slice :2 "${a[@]}")
    0 1
    0 1

    >>> local a=(0 1 2 3 4 5)
    >>> # from position 3 (included) to the end
    >>> echo $(array.slice 3:"${#a[@]}" "${a[@]}")
    >>> echo $(array.slice 3: "${a[@]}")
    3 4 5
    3 4 5

    >>> local a=(0 1 2 3 4 5)
    >>> # from the second-last (included) to the end
    >>> echo $(array.slice -2:"${#a[@]}" "${a[@]}")
    >>> echo $(array.slice -2: "${a[@]}")
    4 5
    4 5

    >>> local a=(0 1 2 3 4 5)
    >>> echo $(array.slice -4:-2 "${a[@]}")
    2 3

    If no range is given, it works like normal array indices.
    >>> local a=(0 1 2 3 4 5)
    >>> echo $(array.slice -1 "${a[@]}")
    5
    >>> local a=(0 1 2 3 4 5)
    >>> echo $(array.slice -2 "${a[@]}")
    4
    >>> local a=(0 1 2 3 4 5)
    >>> echo $(array.slice 0 "${a[@]}")
    0
    >>> local a=(0 1 2 3 4 5)
    >>> echo $(array.slice 1 "${a[@]}")
    1
    >>> local a=(0 1 2 3 4 5)
    >>> array.slice 6 "${a[@]}"; echo $?
    1
    >>> local a=(0 1 2 3 4 5)
    >>> array.slice -7 "${a[@]}"; echo $?
    1
    '
    local start end array_length length
    if [[ $1 == *:* ]]; then
        IFS=":"; read -r start end <<<"$1"
        shift
        array_length="$#"
        # defaults
        [ -z "$end" ] && end=$array_length
        [ -z "$start" ] && start=0
        (( start < 0 )) && let "start=(( array_length + start ))"
        (( end < 0 )) && let "end=(( array_length + end ))"
    else
        start="$1"
        shift
        array_length="$#"
        (( start < 0 )) && let "start=(( array_length + start ))"
        let "end=(( start + 1 ))"
    fi
    let "length=(( end - start ))"
    (( start < 0 )) && return 1
    # check bounds
    (( length < 0 )) && return 1
    (( start < 0 )) && return 1
    (( start >= array_length )) && return 1
    # parameters start with $1, so add 1 to $start
    let "start=(( start + 1 ))"
    echo "${@: $start:$length}"
}
alias array.slice="array_slice"

答案 3 :(得分:1)

假设我正在从用户那里读取一个数组,那么我想看到包含3到7的元素。

cnt=0
while read var;
    do
    myarr[cnt]=$var
    cnt=$((cnt+1)) 
    done


echo ${myarr[@]:3:5}

答案 4 :(得分:0)

冒着打败死马的风险,我受到了@jandob's answer的启发并制作了这个版本

  1. 更简单(没有太多的 shift 逻辑或经常重写变量)。
  2. 尊重带引号的字符串而不处理 IFS(仅限-r 模式)。
  3. 允许用户通过 [start, end) 标志指定 [start, length] 切片或 -l 切片。
  4. 允许您 echo 结果数组(默认行为),或将其“返回”到一个新数组中以供调用父级使用(通过 -r slicedArray)。

注意:namerefs 仅在 Bash >= 4.3 中受支持。要支持较早版本的 Bash(即没有 Brew bash 的 Mac),您需要使用 indirection 代替:使用临时变量访问数组参数,例如declare arrValuesCmd="$1[@]"; declare arr=("${!arrValuesCmd}"),并使用 eval 作为返回值,例如eval $retArrName='("${newArr[@]}")'(注意数组声明周围的单引号)。

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
}

示例:

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