【问题标题】:How to slice an array in Bash如何在 Bash 中对数组进行切片
【发布时间】:2010-11-23 01:51:04
【问题描述】:

查看 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

有没有更好的方法来做到这一点?

【问题讨论】:

    标签: arrays bash slice


    【解决方案1】:

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

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

    注意:namerefs 仅在 Bash >= 4.3 中受支持。要支持早期版本的 Bash(即没有 Brew 的 bash 的 Mac),您需要改用 indirection:使用 temp var 访问数组参数,例如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
    

    【讨论】:

      【解决方案2】:

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

      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 是一个数组元素(并且它包含一个额外的空格)这一事实被保留。

      【讨论】:

      • 酷。我查看了 Array 部分,并没有在那里看到它。
      • @AquariusPower:创建一个索引数组并将其切片:idx=(${!A[@]}); echo ${idx[@]:1}
      • 这个也可以用于类似数组的特殊变量$@吗? ${@[@]:1:2} 给出错误 bad substitution
      • @Feuermurmel:不使用索引方括号即可:${@:1:2}
      • @DennisWilliamson 我发现在执行此操作之前我需要将 $@ 转换为适当的数组,否则包含空格的参数会被拆分:ARGS=( "$@" ); ARGS_AFTER_FIRST=( "${ARGS[@]:1}" )
      【解决方案3】:

      类似 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"
      

      【讨论】:

      • 该死的家伙。道具。
      【解决方案4】:

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

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

      【讨论】:

      • 当你在它的时候:echo "${A[@]::1}" # 4
      • 这很好,但需要注意的是,如果在函数中使用,必须稍作改动以读取"${${@}[@]:1}"
      • @AlexGray:这给了我“糟糕的替代”,但${@:2} 工作正常。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-15
      • 2013-09-20
      • 1970-01-01
      相关资源
      最近更新 更多