Drukowanie macierzy asocjacyjnych BASH


17

Czy istnieje sposób na wydrukowanie całej tablicy ([klucz] = wartość) bez zapętlania wszystkich elementów?

Załóżmy, że utworzyłem tablicę z niektórymi elementami:

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

Mogę wydrukować całą tablicę za pomocą

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

Wydaje się jednak, że bash już wie, jak uzyskać wszystkie elementy tablicy za jednym razem - zarówno klucze, jak ${!array[@]}i wartości ${array[@]}.

Czy istnieje sposób, aby bash wydrukował te informacje bez pętli?

Edycja:
typeset -p arrayrobi to!
Nie mogę jednak usunąć zarówno prefiksu, jak i sufiksu w jednym zastąpieniu:

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

Czy istnieje prostszy sposób na uzyskanie / wydrukowanie tylko części klucz = wartość wyniku?

Odpowiedzi:


15

Myślę, że pytasz o dwie różne rzeczy.

Czy istnieje sposób, aby bash wydrukował te informacje bez pętli?

Tak, ale nie są tak dobre, jak zwykła pętla.

Czy istnieje prostszy sposób na uzyskanie / wydrukowanie tylko części klucz = wartość wyniku?

Tak, forpętla. Ma to tę zaletę, że nie wymaga programów zewnętrznych, jest proste i sprawia, że ​​kontrolowanie dokładnego formatu wyjściowego jest dość łatwe bez niespodzianek.


Każde rozwiązanie, które próbuje obsłużyć dane wyjściowe declare -p( typeset -p), musi dotyczyć: a) możliwości samych zmiennych zawierających nawiasy lub nawiasy, b) cytowania, żedeclare -p należy dodać, aby dane wyjściowe były prawidłowe dla powłoki.

Na przykład twoje rozwinięcie b="${a##*(}"zjada niektóre wartości, jeśli dowolny klucz / wartość zawiera nawias otwierający. Jest tak, ponieważ użyłeś ##, który usuwa najdłuższy prefiks. To samo dotyczy c="${b%% )*}". Chociaż możesz oczywiście dopasować declaredokładnie wydrukowany szablon , nadal będziesz miał trudności, jeśli nie chcesz wszystkich cytatów.

Nie wygląda to zbyt ładnie, chyba że jest to potrzebne.

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

Dzięki forpętli łatwiej jest wybrać format wyjściowy, jak chcesz:

# 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\'

Stamtąd można również w prosty sposób zmienić format wyjściowy (usuń nawiasy wokół klucza, umieść wszystkie pary klucz / wartość w jednym wierszu ...). Jeśli potrzebujesz cytowania czegoś innego niż sama powłoka, nadal musisz to zrobić sam, ale przynajmniej masz surowe dane do pracy. (Jeśli masz nowe znaki w kluczach lub wartościach, prawdopodobnie będziesz potrzebować cytowania).

Przy obecnej wersji Bash (wydaje mi się, że 4.4) możesz także użyć printf "[%s]=%s" "${x@Q}" "${array[$x]@Q}"zamiast printf "%q=%q". Tworzy nieco ładniejszy format cytowany, ale oczywiście jest trochę więcej pracy do zapamiętania, aby pisać. (I cytuje przypadek narożny @jako klucz tablicy, który %qnie cytuje.)

Jeśli pętla for wydaje się zbyt zmęczona, aby pisać, zapisz gdzieś funkcję (bez cytowania tutaj):

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

A następnie użyj tego:

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

Działa również z indeksowanymi tablicami:

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

Zauważ, że wyjście twojego printf ...%q...wariantu nie nadaje się do ponownego włożenia do powłoki, jeśli tablica ma @klucz, ponieważ% q go nie zacytuje i a=([@]=value)jest to błąd składniowy w bash.
Stéphane Chazelas

@ StéphaneChazelas, najwyraźniej. "${x@Q}"cytuje to również, ponieważ cytuje wszystkie ciągi (i wygląda ładniej). dodano notatkę o korzystaniu z tego.
ilkkachu

Tak, skopiowane z mksh. Kolejny operator o innym kształcie, którego nie można łączyć z większością innych. Ponownie zobacz, zshze swoimi zmiennymi flagami ekspansji (które ponownie wyprzedzają bash o dekady i za pomocą których możesz wybrać styl cytowania: $ {(q) var}, $ {(qq) var} ...) dla lepszego projektu. bash ma ten sam problem co mksh, ponieważ nie cytuje pustego ciągu (nie jest to problemem, ponieważ bash nie obsługuje pustych kluczy). Ponadto, przy użyciu cytując style inne niż pojedynczy cudzysłów ( ${var@Q}ośrodki do $'...'niektórych wartości) ważne jest, że kod będzie reinput w tej samej lokalizacji.
Stéphane Chazelas

@ StéphaneChazelas, myślę, że masz na myśli wartość nieustawioną, a nie pusty ciąg? ( x=; echo "${x@Q}"daje '', unset x; echo "${x@Q}"nic nie daje). @QWydaje się, że Bash woli $'\n'od dosłownego nowego wiersza, co w niektórych sytuacjach może być dobre (ale nie wiem, co wolą inni). Oczywiście wybór nie byłby zły.
ilkkachu

Och tak, przepraszam, nie zdawałem sobie z tego sprawy. To różnica w stosunku do mksh. $'...'Składnia jest potencjalnym problemem w takie rzeczy jak LC_ALL=zh_HK.big5hkscs bash -c 'a=$'\''\n\u3b1'\''; printf "%s\n" "${a@Q}"'które wyjścia $'\n<0xa3><0x5c>'i 0x5csam jest backslash tak, że masz wątpliwości, czy to cytat był interpretowany w innej lokalizacji.
Stéphane Chazelas

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

2 widelec

Może to:

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 widelce

albo to:

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

Bez widelca

do porównania

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

Porównanie czasów wykonania

Ponieważ ostatnia składnia nie korzysta z fork, mogą być szybsze:

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

Ale ta afirmacja nie pozostaje prawdziwa, jeśli tablica staje się duża; jeśli redukcja widelców jest wydajna w przypadku małych procesów, użycie dedykowanych narzędzi jest bardziej wydajne w przypadku większych procesów.

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

Uwaga

Ponieważ oba ( rozwidlone ) rozwiązania wykorzystują wyrównanie , żadne z nich nie będzie działać, jeśli jakakolwiek zmienna zawiera nowy wiersz . W takim przypadku jedynym sposobem jest forpętla.


Wyglądają sprytnie, oba sposoby są mniej wydajne niż for. To naprawdę szkoda.
Satō Katsura

@SatoKatsura Zgadzam się, ale jeśli wolniej, użycie składni prjest krótsze ... Nie jestem pewien, czy prskładnia pozostanie wolniejsza, nawet przy dużych tablicach!
F. Hauri

2
@MiniMax Ponieważ nie daje poprawnego wyniku (te same elementy, niewłaściwa kolejność). Trzeba będzie spakować tablice ${!array[@]}i ${array[@]}najpierw to zadziała.
Satō Katsura

1
Ten ostatni fragment z pastejest dłuższy niż forpętla w pytaniu zapisanym w jednym wierszu for i in "${!array[@]}"; do echo "$i=${array[$i]}" ; done, ale wymaga dwóch podpowłok i zewnętrznego programu. Jak to jest schludne? Rozwiązanie z prrównież się psuje, jeśli jest wiele elementów, ponieważ próbuje paginować dane wyjściowe. Musisz użyć czegoś, | pr -2t -l"${#array[@]}"co zaczyna być trudne do zapamiętania w porównaniu do prostej pętli, i znowu jest dłuższe.
ilkkachu

1
W bash, cmd1 | cmd2środki 2 widelce, nawet jeśli cmd1 lub cmd2 lub oba są wbudowane.
Stéphane Chazelas

2

Jeśli szukasz powłoki z lepszą obsługą macierzy asocjacyjnych, spróbuj zsh.

W zsh(gdzie tablice asocjacyjne zostały dodane w 1998 r., W porównaniu do 1993 r. Dla ksh93 i 2009 dla bash), $varlub ${(v)var}rozwija się do (niepustych) wartości skrótu, ${(k)var}do (niepustych) kluczy (w tej samej kolejności), oraz ${(kv)var}do kluczy i wartości.

Aby zachować puste wartości, jak w przypadku tablic, musisz zacytować i użyć @flagi.

Aby wydrukować klucze i wartości, to tylko kwestia

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

Aby uwzględnić ewentualny pusty skrót, powinieneś:

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

Zauważ też, że zsh używa znacznie bardziej sensownej i użytecznej składni definicji tablicy niż ksh93's (kopiowane przez bash):

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

Co znacznie ułatwia kopiowanie lub scalanie tablic asocjacyjnych:

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

(nie można łatwo skopiować skrótu bez pętli bashi należy to zauważyćbash obecnie nie obsługuje pustych kluczy ani kluczy / wartości z bajtami NUL).

Zobacz także zshfunkcje kompresji tablic, których zwykle potrzebujesz do pracy z tablicami asocjacyjnymi:

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

1

Skoro skład tekstu ma to, czego chcesz, dlaczego nie po prostu edytować jego wynik?

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

daje

a2 = 2  
a1 = 1  
b1 = bbb 

Gdzie

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

Pełne, ale dość łatwo jest zobaczyć, jak działa formatowanie: po prostu uruchom potok z stopniowo większą liczbą poleceń sed i tr . Zmodyfikuj je, aby pasowały do ​​ładnych gustów drukowania.


Tego rodzaju potok z pewnością się nie powiedzie, gdy niektóre klucze lub wartości tablicy będą zawierały zastępowane znaki, takie jak nawiasy, nawiasy kwadratowe lub cudzysłowy. A potok seds i trnie jest nawet znacznie prostszy niż forpętla z printf.
ilkkachu

Czy wiesz też, że trtłumaczysz znak po znaku, nie pasuje do ciągów? tr "]=" " ="zmienia „]” na spację i =na =, niezależnie od pozycji. Więc prawdopodobnie możesz po prostu połączyć wszystkie trzy trz jednym.
ilkkachu

Bardzo prawdziwe o niektórych nie alfanumerycznych postaciach, które się w tym grzebią. Jednak wszystko, co musi sobie z nimi poradzić, staje się o rząd wielkości bardziej złożone i mniej czytelne, więc chyba że istnieje naprawdę dobry powód, aby mieć je w pliku danych i jest to określone w pytaniu, które zakładam, że zostały one odfiltrowane, zanim tu dotarliśmy. Powinieneś zawsze mieć swoje wyraźne zastrzeżenie. Uważam, że te potoki są prostsze, na przykład do celów debugowania, niż glob printf, który albo działa idealnie, albo wysadza się w twarz. Tutaj dokonujesz jednej prostej zmiany na element, przetestuj go, a następnie dodaj jeszcze 1.
Nadreck

Mój błąd! Całkowicie pomieszałem moje _tr_s i _sed_s! Naprawiono w najnowszej edycji.
Nadreck

1

Jeszcze jedną opcją jest wyświetlenie wszystkich zmiennych i grep dla tej, którą chcesz.

set | grep -e '^aa='

Używam tego do debugowania. Wątpię, aby był bardzo wydajny, ponieważ zawiera listę wszystkich zmiennych.

Jeśli często to robisz, możesz ustawić taką funkcję:

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

Niestety, gdy sprawdzamy wydajność za pomocą czasu:

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

Dlatego, jeśli robisz to bardzo często, chciałbyś wersji NO FORKS @ F.Hauri, ponieważ jest ona o wiele szybsza.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.