Een array in Bash segmenteren

Kijkend naar de sectie “Array” in de bash(1) man-pagina, vond ik geen manier om een ​​array te slicen.

Dus ik bedacht deze al te ingewikkelde functie:

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

Zo gebruikt:

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

Is er een betere manier om dit te doen?


Antwoord 1, autoriteit 100%

Zie het gedeelte Parameteruitbreidingin de Bash manpagina. A[@]retourneert de inhoud van de array, :1:2neemt een plak van lengte 2, beginnend bij index 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

Merk op dat het feit dat a b céén array-element is (en dat het een extra spatie bevat) behouden blijft.


Antwoord 2, autoriteit 14%

Er is ook een handige snelkoppeling om alle elementen van de array te krijgen die beginnen met de opgegeven index. “${A[@]:1}” zou bijvoorbeeld de “staart” van de array zijn, dat wil zeggen de array zonder zijn eerste element.

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

Antwoord 3

Array-slicing zoals in Python (uit de rebash-bibliotheek):

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"

Antwoord 4

Met het risico een dood paard te verslaan, werd ik geïnspireerd door @jandob’s antwoorden maakte deze versie die

  1. Is eenvoudiger (heeft niet zo veel shift-logica of herschrijven van variabelen zo vaak).
  2. Respecteert tekenreeksen tussen aanhalingstekens zonder gebruik te maken van IFS(alleen modus -r).
  3. Hiermee kan de gebruiker [start, end)slicing of [start, length]slicing specificeren via de vlag -l.
  4. Hiermee kunt u de resulterende array echo(standaardgedrag), of “retourneren” in een nieuwe array voor gebruik in de aanroepende ouder (via -r slicedArray) .

Opmerking: namerefsworden alleen ondersteund in Bash >= 4.3. Om eerdere versies van Bash (dwz Mac zonder Brew’s bash) te ondersteunen, moet je indirectionin plaats daarvan: gebruik een tijdelijke var om toegang te krijgen tot arrayparameters, bijv. declare arrValuesCmd="$1[@]"; declare arr=("${!arrValuesCmd}"), en gebruik eval voor retourwaarden, b.v. eval $retArrName='("${newArr[@]}")'(let op de enkele aanhalingstekens rond de array-declaratie).

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
}

Voorbeelden:

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

Other episodes