Bash - inverte un array


16

Esiste un modo semplice per invertire un array?

#!/bin/bash

array=(1 2 3 4 5 6 7)

echo "${array[@]}"

quindi otterrei: 7 6 5 4 3 2 1
invece di:1 2 3 4 5 6 7

Risposte:


15

Ho risposto alla domanda come scritto e questo codice inverte l'array. (La stampa degli elementi in ordine inverso senza invertire l'array è solo un forloop che esegue il conto alla rovescia dall'ultimo elemento a zero.) Questo è un algoritmo standard di "primo e ultimo scambio".

array=(1 2 3 4 5 6 7)

min=0
max=$(( ${#array[@]} -1 ))

while [[ min -lt max ]]
do
    # Swap current first and last elements
    x="${array[$min]}"
    array[$min]="${array[$max]}"
    array[$max]="$x"

    # Move closer
    (( min++, max-- ))
done

echo "${array[@]}"

Funziona con matrici di lunghezza pari e dispari.


Si noti che ciò non funziona con array sparsi.
Isacco,

@Isaac c'è una soluzione su StackOverflow se devi gestirli.
roaima,

Risolto qui .
Isacco,

18

Un altro approccio non convenzionale:

#!/bin/bash

array=(1 2 3 4 5 6 7)

f() { array=("${BASH_ARGV[@]}"); }

shopt -s extdebug
f "${array[@]}"
shopt -u extdebug

echo "${array[@]}"

Produzione:

7 6 5 4 3 2 1

Se extdebugabilitato, l'array BASH_ARGVcontiene in una funzione tutti i parametri posizionali in ordine inverso.


Questo è un trucco fantastico!
Valentin Bajrami il

15

Approccio non convenzionale (tutto non puro bash):

  • se tutti gli elementi in un array sono solo uno (come nella domanda) puoi usare rev:

    echo "${array[@]}" | rev
  • altrimenti:

    printf '%s\n' "${array[@]}" | tac | tr '\n' ' '; echo
  • e se puoi usare zsh:

    echo ${(Oa)array}

ho appena guardato in alto tac, come il contrario di catabbastanza buono da ricordare, GRAZIE!
Nath,

3
Anche se mi piace l'idea rev, devo dire che revnon funzionerà correttamente per i numeri con due cifre. Ad esempio, un elemento dell'array di 12 utilizzo rev verrà stampato come 21. Provalo ;-)
George Vasiliou il

@GeorgeVasiliou Sì, funzionerà solo se tutti gli elementi sono composti da un carattere (numeri, lettere, punteggiatura, ...). Ecco perché ho dato anche una seconda soluzione più generale.
jimmij,

8

Se si desidera effettivamente il contrario in un altro array:

reverse() {
    # first argument is the array to reverse
    # second is the output array
    declare -n arr="$1" rev="$2"
    for i in "${arr[@]}"
    do
        rev=("$i" "${rev[@]}")
    done
}

Poi:

array=(1 2 3 4)
reverse array foo
echo "${foo[@]}"

dà:

4 3 2 1

Questo dovrebbe gestire correttamente i casi in cui manca un indice di array, diciamo che avevi array=([1]=1 [2]=2 [4]=4), nel qual caso il looping da 0 all'indice più alto può aggiungere ulteriori elementi vuoti.


Grazie per questo, funziona abbastanza bene, anche se per qualche motivo shellcheckstampa due avvertimenti: array=(1 2 3 4) <-- SC2034: array appears unused. Verify it or export it.e per:echo "${foo[@]}" <-- SC2154: foo is referenced but not assigned.
nath,

1
@nath sono usati indirettamente, ecco a cosa declareserve la linea.
Muru,

Intelligente, ma nota che declare -nsembra non funzionare nelle versioni bash precedenti alla 4.3.
G-Man dice "Ripristina Monica" il

8

Per scambiare le posizioni dell'array in posizione (anche con array sparsi) (da bash 3.0):

#!/bin/bash
# Declare an sparse array to test:
array=([5]=101 [6]=202 [10]=303 [11]=404 [20]=505 [21]=606 [40]=707)
echo "Initial array values"
declare -p array

swaparray(){ local temp; temp="${array[$1]}"
             array[$1]="${array[$2]}"
             array[$2]="$temp"
           }

ind=("${!array[@]}")                         # non-sparse array of indexes.

min=-1; max="${#ind[@]}"                     # limits to one before real limits.
while [[ min++ -lt max-- ]]                  # move closer on each loop.
do
    swaparray "${ind[min]}" "${ind[max]}"    # Exchange first and last
done

echo "Final Array swapped in place"
declare -p array
echo "Final Array values"
echo "${array[@]}"

In esecuzione:

./script
Initial array values
declare -a array=([5]="101" [6]="202" [10]="303" [11]="404" [20]="505" [21]="606" [40]="707")

Final Array swapped in place
declare -a array=([5]="707" [6]="606" [10]="505" [11]="404" [20]="303" [21]="202" [40]="101")

Final Array values
707 606 505 404 303 202 101

Per bash più vecchio, devi usare un loop (in bash (dal 2.04)) e usare $aper evitare lo spazio finale:

#!/bin/bash

array=(101 202 303 404 505 606 707)
last=${#array[@]}

a=""
for (( i=last-1 ; i>=0 ; i-- ));do
    printf '%s%s' "$a" "${array[i]}"
    a=" "
done
echo

Per bash dal 2.03:

#!/bin/bash
array=(101 202 303 404 505 606 707)
last=${#array[@]}

a="";i=0
while [[ last -ge $((i+=1)) ]]; do 
    printf '%s%s' "$a" "${array[ last-i ]}"
    a=" "
done
echo

Inoltre (usando l'operatore di negazione bit a bit) (dalla bash 4.2+):

#!/bin/bash
array=(101 202 303 404 505 606 707)
last=${#array[@]}

a=""
for (( i=0 ; i<last ; i++ )); do 
    printf '%s%s' "$a" "${array[~i]}"
    a=" "
done
echo

Affrontare gli elementi di un array dalla fine all'indietro con sottoscrizioni negative sembra non funzionare nelle versioni bash precedenti alla 4.3.
G-Man dice "Ripristina Monica" il

1
In realtà, indirizzare i numeri negativi è stato modificato in 4.2-alpha. E lo script con valori negati funziona da quella versione. @ G-Man p. Sottoscrizioni negative ad array indicizzati, ora trattati come offset dall'indice massimo assegnato + 1. ma i Bash-hacker riportano erroneamente 4.1 array indicizzati numericamente sono accessibili dalla fine usando indici negativi
Isaac

3

Brutto, non mantenibile, ma univoco:

eval eval echo "'\"\${array['{$((${#array[@]}-1))..0}']}\"'"

Non più semplice, ma più breve: eval eval echo "'\"\${array[-'{1..${#array[@]}}']}\"'".
Isaac,

E anche per matrici sparse:ind=("${!array[@]}");eval eval echo "'\"\${array[ind[-'{1..${#array[@]}}']]}\"'"
Isaac il

@Isaac Ma non è più un problema e solo brutto e non mantenibile per la versione sparsa dell'array, sfortunatamente. (Dovrebbe essere comunque più veloce dei tubi per piccoli array).
user23013

Bene, tecnicamente, è un "one-liner"; non è un comando, sì, ma un "one liner" lo è. Sono d'accordo, sì, molto brutto e un problema di manutenzione, ma divertente con cui giocare.
Isaac,

1

Anche se non ho intenzione di dire qualcosa di nuovo e userò anche tacper invertire l'array, penso che varrebbe la pena menzionare la seguente soluzione a linea singola usando bash versione 4.4:

$ read -d'\n' -a array < <(printf '%s\n' "${array[@]}" |tac)

test:

$ array=(1 2 3 4 5 6 10 11 12)
$ echo "${array[@]}"
1 2 3 4 5 6 10 11 12
$ read -d'\n' -a array < <(printf '%s\n' "${array[@]}"|tac)
$ echo "${array[@]}"
12 11 10 6 5 4 3 2 1

Ricorda che il nome var all'interno di read è il nome dell'array originale, quindi per l'archiviazione temporanea non è necessario alcun array helper.

Implementazione alternativa regolando IFS:

$ IFS=$'\n' read -d '' -a array < <(printf '%s\n' "${array[@]}"|tac);declare -p array
declare -a array=([0]="12" [1]="11" [2]="10" [3]="6" [4]="5" [5]="4" [6]="3" [7]="2" [8]="1")

PS: Penso che le soluzioni precedenti non funzioneranno nella bashversione qui sotto a 4.4causa della diversa readimplementazione della funzione integrata bash.


La IFSversione funziona, ma è anche la stampa: declare -a array=([0]="1" [1]="2" [2]="3" [3]="4" [4]="5" [5]="6" [6]="10" [7]="11" [8]="12"). Usando bash 4.4-5. Devi rimuovere ;declare -p arrayalla fine della prima riga, quindi funziona ...
Nath

1
@nath declare -pè solo un modo rapido per fare in modo che bash stampi il vero array (indice e contenuti). Non hai bisogno di questo declare -pcomando nel tuo vero script. Se qualcosa va storto nelle assegnazioni di array, potresti finire nel caso in cui ${array[0]}="1 2 3 4 5 6 10 11 12"= tutti i valori siano memorizzati nello stesso indice - usando l'eco non vedrai alcuna differenza. Per una rapida stampa dell'array, l'utilizzo declare -p arrayrestituirà gli indici reali dell'array e il valore corrispondente in ciascun indice.
George Vasiliou,

@nath A proposito, il read -d'\n'metodo non ha funzionato per te?
George Vasiliou,

read -d'\n'funziona bene.
nath,

ahhh ti ho preso! SPIACENTE :-)
nath,

1

Per invertire una matrice arbitraria (che può contenere un numero qualsiasi di elementi con qualsiasi valore):

Con zsh:

array_reversed=("${(@Oa)array}")

Con bash4.4+, dato che le bashvariabili non possono contenere comunque byte NUL, puoi usare GNU tac -s ''sugli elementi stampati come record delimitati da NUL:

readarray -td '' array_reversed < <(
  ((${#array[@]})) && printf '%s\0' "${array[@]}" | tac -s '')

POSIX, per invertire l'array di shell POSIX ( $@, composto da $1, $2...):

code='set --'
n=$#
while [ "$n" -gt 0 ]; do
  code="$code \"\${$n}\""
  n=$((n - 1))
done
eval "$code"

1

Pura soluzione di bash, funzionerebbe come una fodera.

$: for (( i=${#array[@]}-1; i>=0; i-- ))
>  do rev[${#rev[@]}]=${array[i]}
>  done
$: echo  "${rev[@]}"
7 6 5 4 3 2 1

Ben fatto!!! GRAZIE; qui l'unico liner da copiare :-) `array = (1 2 3 4 5 6 7); for ((i = $ {# array [@]} - 1; i> = 0; i--)); esegui rev [$ {# rev [@]}] = $ {array [i]}; fatto; echo "$ {rev [@]}" `
nath

Fare rev+=( "${array[i]}" )sembra più semplice.
Isaac,

Sei di uno, mezza dozzina dell'altro. Non sono provvisto di quella sintassi, ma non ho motivo per farlo - solo pregiudizio e preferenza. Tu lo fai.
Paul Hodges,

-1

puoi anche considerare l'utilizzo seq

array=(1 2 3 4 5 6 7)

for i in $(seq $((${#array[@]} - 1)) -1 0); do
    echo ${array[$i]}
done

in freebsd puoi omettere -1 parametro di incremento:

for i in $(seq $((${#array[@]} - 1)) 0); do
    echo ${array[$i]}
done

Si noti che ciò non inverte l'array, ma semplicemente lo stampa in ordine inverso.
roaima,

D'accordo, il mio punto era anche considerare l'accesso agli indici come un'alternativa.
M. Modugno il

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.