Stampa array associativa BASH


17

C'è un modo per stampare un intero array ([chiave] = valore) senza passare in rassegna tutti gli elementi?

Supponiamo di aver creato un array con alcuni elementi:

declare -A array
array=([a1]=1 [a2]=2 ... [b1]=bbb ... [f500]=abcdef)

Posso stampare l'intero array con

for i in "${!array[@]}"
do
echo "${i}=${array[$i]}"
done

Tuttavia, sembra che bash sappia già come ottenere tutti gli elementi dell'array in un unico "go", sia chiavi ${!array[@]}che valori ${array[@]}.

C'è un modo per fare in modo che bash stampi queste informazioni senza il ciclo?

Modifica: lo
typeset -p arrayfa!
Tuttavia non riesco a rimuovere sia il prefisso che il suffisso in una singola sostituzione:

a="$(typeset -p array)"
b="${a##*(}"
c="${b%% )*}"

Esiste un modo più pulito per ottenere / stampare solo la parte chiave = valore dell'output?

Risposte:


15

Penso che ci stai chiedendo due cose diverse lì.

C'è un modo per fare in modo che bash stampi queste informazioni senza il ciclo?

Sì, ma non sono buoni come usare il loop.

Esiste un modo più pulito per ottenere / stampare solo la parte chiave = valore dell'output?

Sì, il forciclo. Ha i vantaggi di non richiedere programmi esterni, è semplice e semplifica il controllo del formato di output esatto senza sorprese.


Qualsiasi soluzione che tenti di gestire l'output di declare -p( typeset -p) ha a che fare con a) la possibilità che le variabili stesse contengano parentesi o parentesi, b) la quotazione che declare -pdeve aggiungere per rendere l'input valido per l'input della shell.

Ad esempio, l'espansione b="${a##*(}"mangia alcuni dei valori, se una chiave / valore contiene una parentesi aperta. Questo perché hai usato ##, che rimuove il prefisso più lungo . Lo stesso per c="${b%% )*}". Sebbene tu possa ovviamente abbinare la piastra stampata in modo declarepiù preciso, avresti comunque difficoltà se non volessi tutte le quotazioni che fa.

Non sembra molto bello a meno che tu non ne abbia bisogno.

$ declare -A array=([abc]="'foobar'" [def]='"foo bar"')
$ declare -p array
declare -A array='([def]="\"foo bar\"" [abc]="'\''foobar'\''" )'

Con il forloop, è più facile scegliere il formato di output come preferisci:

# without quoting
$ for x in "${!array[@]}"; do printf "[%s]=%s\n" "$x" "${array[$x]}" ; done
[def]="foo bar"
[abc]='foobar'

# with quoting
$ for x in "${!array[@]}"; do printf "[%q]=%q\n" "$x" "${array[$x]}" ; done
[def]=\"foo\ bar\"
[abc]=\'foobar\'

Da lì, è anche semplice cambiare il formato di output altrimenti (rimuovere le parentesi attorno alla chiave, mettere tutte le coppie chiave / valore su una sola riga ...). Se hai bisogno di un preventivo per qualcosa di diverso dalla shell stessa, dovrai comunque farlo da solo, ma almeno hai i dati grezzi su cui lavorare. (Se hai nuove righe nelle chiavi o nei valori, probabilmente avrai bisogno di un preventivo.)

Con un Bash attuale (4.4, credo), potresti anche usare printf "[%s]=%s" "${x@Q}" "${array[$x]@Q}"invece di printf "%q=%q". Produce un formato citato un po 'più bello, ma ovviamente è un po' più di lavoro da ricordare di scrivere. (E cita il caso d'angolo di @come chiave dell'array, che %qnon cita.)

Se il ciclo for sembra troppo stanco per scrivere, salvalo da qualche parte (senza citare qui):

printarr() { declare -n __p="$1"; for k in "${!__p[@]}"; do printf "%s=%s\n" "$k" "${__p[$k]}" ; done ;  }  

E poi basta usare quello:

$ declare -A a=([a]=123 [b]="foo bar" [c]="(blah)")
$ printarr a
a=123
b=foo bar
c=(blah)

Funziona anche con array indicizzati:

$ b=(abba acdc)
$ printarr b
0=abba
1=acdc

Si noti che l'output della printf ...%q...variante non è adatto per il reinput alla shell se l'array ha una @chiave poiché% q non la cita ed a=([@]=value)è un errore di sintassi bash.
Stéphane Chazelas,

@ StéphaneChazelas, a quanto pare. "${x@Q}"cita anche questo, dal momento che cita tutte le stringhe (e sembra più bello). aggiunto una nota sull'uso di questo.
ilkkachu,

Sì, copiato da mksh. Un altro operatore di forma ancora diversa che non può essere combinato con la maggior parte degli altri. Ancora una volta, guarda zshcon i suoi flag di espansione variabili (che precedono ancora di bash di decenni e con i quali puoi scegliere lo stile di quotazione: $ {(q) var}, $ {(qq) var} ...) per un design migliore. bash ha lo stesso problema di mksh in quanto non cita la stringa vuota (non un problema qui dato che comunque bash non supporta le chiavi vuote). Inoltre, quando si utilizzano stili di citazione diversi dalla virgoletta singola ( ${var@Q}ricorre ad $'...'alcuni valori) è importante che il codice venga reintegrato nella stessa locale.
Stéphane Chazelas,

@ StéphaneChazelas, penso che intendi un valore non impostato, non una stringa vuota? ( x=; echo "${x@Q}"non dà '', unset x; echo "${x@Q}"non dà niente.) Bash @Qsembra preferire $'\n'una nuova riga letterale, che in realtà può essere buona in alcune situazioni (ma non posso dire cosa preferiscono gli altri). Ovviamente avere una scelta non sarebbe male.
ilkkachu,

Oh sì scusa, non me ne ero reso conto. Questa è una differenza da Mksh allora. La $'...'sintassi è un potenziale problema in cose come LC_ALL=zh_HK.big5hkscs bash -c 'a=$'\''\n\u3b1'\''; printf "%s\n" "${a@Q}"'quali output $'\n<0xa3><0x5c>'e 0x5cda soli è una barra rovesciata, quindi avresti un problema se quella citazione fosse interpretata in una diversa localizzazione.
Stéphane Chazelas,

9
declare -p array
declare -A array='([a2]="2" [a1]="1" [zz]="Hello World" [b1]="bbb" [f50]="abcd" )'

2 forchette

Forse questo:

printf "%s\n" "${!array[@]}"
a2
a1
f50
zz
b1

printf "%s\n" "${array[@]}"
2
1
abcd
Hello World
bbb

printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t
a2                              2
a1                              1
f50                             abcd
zz                              Hello World
b1                              bbb

3 forchette

o questo:

paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}")
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Nessuna forchetta

da confrontare con

for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Confronto dei tempi di esecuzione

Poiché l'ultima sintassi non utilizza fork, potrebbero essere più veloci:

time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
      5      11      76
real    0m0.005s
user    0m0.000s
sys     0m0.000s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
      5       6      41
real    0m0.008s
user    0m0.000s
sys     0m0.000s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
      5       6      41
real    0m0.002s
user    0m0.000s
sys     0m0.001s

Ma questa affermazione non rimane vera se l'array diventa grande; se ridurre le forche è efficiente per piccoli processi, l'uso di strumenti dedicati è più efficiente per processi più grandi.

for i in {a..z}{a..z}{a..z};do array[$i]=$RANDOM;done


time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
  17581   35163  292941
real    0m0.150s
user    0m0.124s
sys     0m0.036s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
  17581   17582  169875
real    0m0.140s
user    0m0.000s
sys     0m0.004s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
  17581   17582  169875
real    0m0.312s
user    0m0.268s
sys     0m0.076s

osservazione

Poiché entrambe le soluzioni (a forcella ) utilizzano l' allineamento , nessuna di esse funzionerà se una variabile contiene una nuova riga . In questo caso, l'unico modo è un forloop.


Mentre sembra intelligente, entrambi i modi sono meno efficienti di a for. È un peccato, davvero.
Satō Katsura,

@SatoKatsura Sono d'accordo, ma se più lento, l'uso della sintassi prè più breve ... Non sono sicuro che la prsintassi rimanga più lenta, anche con array di grandi dimensioni!
F. Hauri,

2
@MiniMax Perché non produce il risultato corretto (stessi elementi, ordine sbagliato). Dovresti comprimere gli array ${!array[@]}e ${array[@]}prima che funzioni.
Satō Katsura,

1
L'ultimo snippet con pasteè più lungo del forloop nella domanda scritta su una riga for i in "${!array[@]}"; do echo "$i=${array[$i]}" ; done, ma richiede due subshells e un programma esterno. Com'è quello più ordinato? La soluzione prsi interrompe anche se ci sono molti elementi, in quanto tenta di impaginare l'output. Avresti bisogno di usare qualcosa di simile | pr -2t -l"${#array[@]}"che sta iniziando a diventare difficile da ricordare rispetto al semplice ciclo e, di nuovo, è più lungo di esso.
ilkkachu,

1
In bash, cmd1 | cmd2mezzi 2 forchette, anche se cmd1 o cmd2 o entrambi sono incorporato.
Stéphane Chazelas,

2

Se stai cercando una shell con un migliore supporto dell'array associativo, prova zsh.

In zsh(dove sono stati aggiunti array associativi nel 1998, rispetto al 1993 per ksh93 e 2009 per bash), $varo si ${(v)var}espande ai valori (non vuoti) dell'hash, ${(k)var}alle chiavi (non vuote) (nello stesso ordine), e ${(kv)var}sia per le chiavi che per i valori.

Per conservare i valori vuoti, come per le matrici, è necessario citare e utilizzare la @bandiera.

Quindi, per stampare chiavi e valori, è solo questione di

printf '%s => %s\n' "${(@kv)var}"

Anche se per tenere conto di un hash possibilmente vuoto, dovresti fare:

(($#var)) &&  printf '%s => %s\n' "${(@kv)var}"

Nota anche che zsh usa una sintassi di definizione dell'array molto più sensata e utile di quella ksh93(copiata da bash):

typeset -A var
var=(k1 v1 k2 v2 '' empty '*' star)

Il che rende molto più semplice copiare o unire array associativi:

var2=("${(@kv)var1}")
var3+=("${(@kv)var2}")
var4=("${@kv)var4}" "${(@kv)var5}")

(non puoi facilmente copiare un hash senza un ciclo con bashe nota che bashattualmente non supporta chiavi vuote o chiave / valori con byte NUL).

Vedi anche le zshfunzionalità di zippatura di array che in genere dovrai lavorare con array associativi:

keys=($(<keys.txt)) values=($(<values.txt))
hash=(${keys:^values})

1

Dal momento che la composizione fa quello che vuoi perché non modificarne solo l'output?

typeset -p array | sed s/^.*\(// | tr -d ")\'\""  | tr "[" "\n" | sed s/]=/' = '/

a2 = 2  
a1 = 1  
b1 = bbb 

Dove

array='([a2]="2" [a1]="1" [b1]="bbb" )'

Verbo ma è abbastanza facile vedere come funziona la formattazione: basta eseguire la pipeline con progressivamente più comandi sed e tr . Modificali per soddisfare i gusti della stampa.


Quel tipo di pipeline è destinato a fallire nel momento in cui alcune delle chiavi o dei valori dell'array contengono uno dei caratteri che stai sostituendo, come parentesi, parentesi o virgolette. E una pipeline di seds e trs' non è ancora molto più semplice di un forciclo con printf.
ilkkachu,

Inoltre, sai che trtradurre carattere per carattere, non corrisponde alle stringhe? tr "]=" " ="cambia "]" in uno spazio e =in uno =, indipendentemente dalla posizione. Quindi probabilmente potresti semplicemente combinare tutti e tre trin uno.
ilkkachu,

Molto vero su alcuni dei caratteri non alfanumerici che lo riassumono. Tuttavia, tutto ciò che deve affrontarli diventa un ordine di grandezza più complesso e meno leggibile, quindi a meno che non ci sia davvero un buon motivo per averli nel tuo feed di dati e questo è indicato nella domanda presumo che siano filtrati prima di arrivare qui. Dovresti sempre avere il tuo esplicito avvertimento però. Trovo che queste condutture siano più semplici, ad esempio e per scopi di debug, rispetto a un globo di stampa che funziona perfettamente o ti esplode in faccia. Qui fai una semplice modifica per elemento, testalo, quindi aggiungi 1 altro.
Nadreck,

Colpa mia! Ho i miei _tr_s e _sed_s totalmente confusi! Risolto nell'ultima modifica.
Nadreck,

1

Un'altra opzione è quella di elencare tutte le variabili e grep per quella desiderata.

set | grep -e '^aa='

Lo uso per il debug. Dubito che sia molto performante poiché elenca tutte le variabili.

Se lo facessi spesso potresti farlo funzionare in questo modo:

aap() { set | grep -e "^$1="; }

Sfortunatamente quando controlliamo le prestazioni usando il tempo:

$ time aap aa aa=([0]="abc") . real 0m0.014s user 0m0.003s sys 0m0.006s

Pertanto, se lo facessi molto spesso, vorresti la versione NO FORKS di @ F.Hauri perché è molto più veloce.

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.