Друк асоціативного масиву BASH


17

Чи є спосіб надрукувати весь масив ([ключ] = значення), не перебираючи всі елементи?

Припустимо, я створив масив з деякими елементами:

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

Я можу надрукувати назад весь масив за допомогою

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

Однак, схоже, bash вже знає, як отримати всі елементи масиву за один "go" - і ключі, ${!array[@]}і значення ${array[@]}.

Чи є спосіб зробити баш друкувати цю інформацію без циклу?

Редагувати:
typeset -p arrayробить це!
Однак я не можу видалити і префікс, і суфікс в одній заміні:

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

Чи є більш чистий спосіб отримати / надрукувати лише ключ = частина вартості виводу?

Відповіді:


15

Я думаю, ти запитуєш дві різні речі там.

Чи є спосіб зробити баш друкувати цю інформацію без циклу?

Так, але вони не такі гарні, як просто використання циклу.

Чи є більш чистий спосіб отримати / надрукувати лише ключ = частина вартості виводу?

Так, forпетля. Він має ті переваги, що не вимагає зовнішніх програм, є простим і дозволяє легко контролювати точний вихідний формат без сюрпризів.


Будь-яке рішення, яке намагається обробити висновок declare -p( typeset -p), має мати справу з a) можливістю самих змінних, що містять дужки або дужки, b) цитування, declare -pяке слід додати, щоб зробити його вихід правильним введенням для оболонки.

Наприклад, розширення b="${a##*(}"з'їдає деякі значення, якщо якийсь ключ / значення містить круглі дужки. Це тому, що ви використовували ##, що видаляє найдовший префікс. Те саме для c="${b%% )*}". Хоча, звичайно, ви могли б declareточно відповідати котлові, надрукованій більш точно, вам все одно буде важко, якби ви не хотіли, щоб усі цитування цього були.

Це не виглядає дуже приємно, якщо вам це не потрібно.

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

За допомогою forциклу простіше вибрати вихідний формат, як вам подобається:

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

Звідти також просто змінити вихідний формат інакше (видаліть дужки навколо клавіші, покладіть всі пари клавіш / значень на один рядок ...). Якщо вам потрібно цитувати щось інше, ніж саму оболонку, вам все одно доведеться робити це самостійно, але принаймні у вас є необроблені дані, над якими можна працювати. (Якщо у вас є нові рядки в ключах або значеннях, вам, ймовірно, знадобиться деяке цитування.)

З поточним Bash (4.4, я думаю) ви також можете використовувати printf "[%s]=%s" "${x@Q}" "${array[$x]@Q}"замість printf "%q=%q". Він створює дещо приємніший формат котирування, але, звичайно, трохи більше роботи, що потрібно пам'ятати, щоб написати. (І він цитує кутовий випадок @як ключ масиву, який %qне цитує.)

Якщо цикл for здається занадто стомленим для запису, збережіть його десь (не цитуючи тут):

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

А потім просто використовуйте це:

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

Працює і з індексованими масивами:

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

Зауважте, що результат вашого printf ...%q...варіанту не підходить для повторного введення в оболонку, якщо масив має @ключ, оскільки% q не цитує його і a=([@]=value)є синтаксичною помилкою в bash.
Стефан Шазелас

@ StéphaneChazelas, мабуть. "${x@Q}"цитує це теж, оскільки він цитує всі рядки (і виглядає приємніше). додав примітку про використання цього.
ilkkachu

Так, скопійовано з mksh. Ще один оператор ще різної форми, який неможливо поєднати з більшістю інших. Знову ж таки, подивіться zshз його флагами змінної розширення (що знову передує bash's десятиліттям, і за допомогою якого ви можете вибрати стиль цитування: $ {(q) var}, $ {(qq) var} ...) для кращого дизайну. bash має ту саму проблему, що і mksh, оскільки він не цитує порожню рядок (не проблема тут, як інакше bash не підтримує порожні ключі). Крім того, при використанні стилів цитування, окрім одинарної цитати ( ${var@Q}вдаються до $'...'деяких значень), важливо, щоб код був повторно введений в одній локалі.
Стефан Шазелас

@ StéphaneChazelas, я думаю, ви маєте на увазі невстановлене значення, а не порожній рядок? ( x=; echo "${x@Q}"не дає '', unset x; echo "${x@Q}"нічого не дає.) Баш, @Qздається, вважає $'\n'за краще над буквальним новим рядком, що може бути хорошим в деяких ситуаціях (але я не можу сказати, що віддають перевагу інші). Звичайно, вибір не був би поганим.
ilkkachu

О так вибачте, я цього не зрозумів. Це відмінність від mksh тоді. $'...'Синтаксис є потенційною проблемою в таких речах , як , LC_ALL=zh_HK.big5hkscs bash -c 'a=$'\''\n\u3b1'\''; printf "%s\n" "${a@Q}"'які виходи $'\n<0xa3><0x5c>'і 0x5cсам по собі є зворотним косими рисами , так що ви повинні були б проблеми , якщо це цитата була інтерпретовані в іншій місцевості.
Стефан Шазелас

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

2 виделки

Можливо, це:

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 виделки

або це:

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

Без виделки

порівнювати з

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

Порівняння часу виконання

Оскільки останній синтаксис не використовує вилку, вони можуть бути швидшими:

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

Але це твердження не залишається істинним, якщо масив стає великим; якщо зменшення вилок ефективно для малого процесу, використання спеціалізованих інструментів є більш ефективним для більшого процесу.

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

Зауваження

Оскільки обидва ( роздвоєні ) рішення використовують вирівнювання , жодне з них не буде працювати, якщо будь-яка змінна містить новий рядок . У цьому випадку єдиний спосіб - це forпетля.


Не дивлячись на розумні, обидва способи є менш ефективними, ніж а for. Що насправді соромно.
Satō Katsura

@SatoKatsura Я згоден, але якщо повільніше, використання синтаксису prкоротше ... Я не впевнений, що prсинтаксис залишається повільніше, навіть із великими масивами!
Ф. Хаурі

2
@MiniMax Оскільки він не дає правильного результату (ті ж елементи, неправильний порядок). Вам знадобиться заархівувати масиви ${!array[@]}і ${array[@]}спочатку для цього попрацювати.
Satō Katsura

1
Цей останній фрагмент з pasteце більше , ніж forцикл в питанні написано на одній лінії for i in "${!array[@]}"; do echo "$i=${array[$i]}" ; done, але вимагає два подоболочки і зовнішньої програми. Як це акуратніше? Рішення з prтакож розривається, якщо є багато елементів, оскільки воно намагається богувати вихід. Вам потрібно буде використовувати щось на зразок, | pr -2t -l"${#array[@]}"яке починає важко запам'ятовувати порівняно з простим циклом, і знову ж таки, довше за нього.
ilkkachu

1
В bash, з cmd1 | cmd2допомогою 2 вилок, навіть якщо cmd1 або cmd2 або обидва вбудованих.
Стефан Шазелас

2

Якщо ви шукаєте оболонку з кращою підтримкою асоціативного масиву, спробуйте zsh.

В zsh(де асоціативні масиви додані у 1998 р. Порівняно з 1993 р. За кш93 та 2009 р. Для баш) $varабо ${(v)var}розширюється до (не порожніх) значень хеша, ${(k)var}до (не порожніх) клавіш (у тому самому порядку), і ${(kv)var}до ключів, і до значень.

Щоб зберегти порожні значення, як для масивів, вам потрібно процитувати та використати @прапор.

Тож надрукувати ключі та значення - це лише питання

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

Хоча для обліку можливого порожнього хешу, ви повинні зробити:

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

Також зауважте, що zsh використовує набагато більш розумний і корисний синтаксис визначення масиву, ніж ksh93's (скопійовано bash):

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

Що значно спрощує копіювання або об'єднання асоціативних масивів:

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

(ви не можете легко скопіювати хеш без циклу bash, і зауважте, що в bashданий час не підтримуються порожні ключі або ключ / значення з байтами NUL).

Дивіться також zshфункції зшивання масивів, які вам зазвичай знадобляться для роботи з асоціативними масивами:

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

1

Оскільки набір робить те, що ви хочете, чому б не просто змінити його вихід?

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

дає

a2 = 2  
a1 = 1  
b1 = bbb 

Де

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

Докладний, але досить легко побачити, як працює форматування: просто виконайте конвеєр із поступово більшою кількістю команд sed і tr . Змініть їх на смаки друку.


Такий конвеєр буде вичерпаний з моменту, коли деякі ключі або значення масиву містять будь-який із символів, які ви замінюєте, як дужки, дужки чи лапки. А трубопровід seds і trs навіть не набагато простіший, ніж forцикл printf.
ilkkachu

Крім того, ви знаєте, що trпереклад символів за символом не відповідає рядкам? tr "]=" " ="змінює "]" на пробіл та на "а" =на " =незалежно від позиції". Тож ви могли, мабуть, просто поєднати всі три trдо одного.
ilkkachu

Дуже правда про деякі нелітерно-цифрові символи, що здумують це. Однак все, що має справу з ними, стає на порядок складнішим та менш читабельним, тому, якщо немає справді вагомих причин занести їх у свій канал даних, і це зазначено у питанні, я припускаю, що вони відфільтровані до того, як ми потрапили сюди. Завжди повинен бути чіткий застереження тхо. Я вважаю, що ці трубопроводи є більш простими, наприклад, з метою налагодження, ніж глобул printf, який або ідеально працює, або вибухає вам в обличчя. Тут ви вносите одну просту зміну на елемент, тестуєте її, а потім додаєте ще 1.
Надрецьк

Моє ліжко! Отримали мої _tr_s та _sed_s повністю змішані! Виправлено в останньому редагуванні.
Надрецьк

1

Ще одним варіантом є перелік усіх змінних та прив'язка до потрібної.

set | grep -e '^aa='

Я використовую це для налагодження. Я сумніваюся, що це дуже ефективно, оскільки в ньому перераховані всі змінні.

Якщо ви робили це часто, ви можете зробити його такою функцією:

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

На жаль, коли ми перевіряємо продуктивність, використовуючи час:

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

Тому, якщо ви робили це дуже часто, ви хотіли б не використовувати версію NO FORKS @ F.Hauri, оскільки це набагато швидше.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.