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 уже знает, как получить все элементы массива за один раз - и ключи, ${!array[@]}и значения ${array[@]}.

Есть ли способ заставить bash распечатать эту информацию без цикла?

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

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

Есть ли более чистый способ получить / напечатать только часть ключа = значение вывода?

Dani_l
источник

Ответы:

15

Я думаю, что вы спрашиваете две разные вещи там.

Есть ли способ заставить bash распечатать эту информацию без цикла?

Да, но они не так хороши, как просто использование цикла.

Есть ли более чистый способ получить / напечатать только часть ключа = значение вывода?

Да, forпетля. Он имеет преимущества, заключающиеся в том, что ему не требуются внешние программы, он прост и позволяет довольно просто контролировать точный формат вывода без неожиданностей.


Любое решение, которое пытается обработать вывод declare -p( typeset -p), должно иметь дело с а) возможностью того, что сами переменные содержат скобки или скобки, б) кавычки, которые 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
ilkkachu
источник
Обратите внимание, что выходные данные вашего printf ...%q...варианта не подходят для повторного ввода в оболочку, если массив имеет @ключ, так как% q его не заключает в кавычки и a=([@]=value)является ошибкой синтаксиса в bash.
Стефан
@ StéphaneChazelas, по-видимому. "${x@Q}"цитирует это тоже, поскольку он цитирует все строки (и выглядит лучше). добавил примечание об использовании этого.
ilkkachu
Да, скопировал с мкш. Еще один оператор еще другой формы, который не может быть объединен с большинством других. Опять же, посмотрите zshс его флагами расширения переменных (которые снова предшествуют bash на десятилетия и с помощью которых вы можете выбрать стиль цитирования: $ {(q) var}, $ {(qq) var} ...) для лучшего дизайна. bash имеет ту же проблему, что и mksh, в том, что он не заключает в кавычки пустую строку (здесь проблема не возникает, так как в любом случае bash не поддерживает пустые ключи). Кроме того, при использовании стилей кавычек, отличных от одинарных кавычек ( ${var@Q}прибегает к $'...'некоторым значениям), важно, чтобы код повторно вводился в той же локали.
Стефан
@ StéphaneChazelas, я думаю, вы имеете в виду неустановленное значение, а не пустую строку? ( x=; echo "${x@Q}"даёт '', unset x; echo "${x@Q}"ничего не дает.) @QКажется, Bash предпочитает $'\n'буквальный перевод строки, который в некоторых ситуациях может быть хорошим (но я не могу сказать, что предпочитают другие). Конечно, выбор там не будет плохим.
ilkkachu
О, да, извини, я этого не поняла. Это отличие от Мкш тогда. $'...'Синтаксис является потенциальной проблемой в таких вещах , как , 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

Сравнение времени выполнения

Поскольку последний синтаксис не использует fork, они могут быть быстрее:

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петля.

Ф. Хаури
источник
Выглядя умно, оба способа менее эффективны, чем a for. Который является позором, действительно.
Satō
@SatoKatsura Я согласен, но если медленнее, использование синтаксиса prбудет короче ... Я не уверен, что prсинтаксис остается медленнее, даже с большими массивами!
Ф. Хаури
2
@MiniMax Потому что это не дает правильный результат (те же элементы, неправильный порядок). Вы должны были бы сжать массивы ${!array[@]}и ${array[@]}сначала для этого работать.
Satō
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 г. для ksh93 и 2009 г. для bash) $varили ${(v)var}расширяются до (непустых) значений хэша, ${(k)var}до (непустых) ключей (в том же порядке), и ${(kv)var}оба ключа и значения.

Чтобы сохранить пустые значения, как для массивов, вам нужно заключить в кавычки и использовать @флаг.

Таким образом, чтобы напечатать ключи и значения, это просто вопрос

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

Хотя, чтобы учесть возможно пустой хеш, вы должны сделать:

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

Также обратите внимание, что zsh использует гораздо более разумный и полезный синтаксис определения массива, чем ksh93s (скопированный 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 делает то, что вы хотите, почему бы просто не отредактировать его вывод?

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 . Измените их, чтобы они соответствовали красивым вкусам печати.

Nadreck
источник
Этот тип конвейера обязательно потерпит неудачу в тот момент, когда некоторые ключи или значения массива содержат какие-либо символы, которые вы заменяете, такие как скобки, скобки или кавычки. И конвейер seds и trs не намного проще, чем forцикл с printf.
ilkkachu
Кроме того, вы знаете, что trпереводить символ за символом, это не соответствует строки? tr "]=" " ="меняет "]" на пробел и a =на a =, независимо от положения. Таким образом, вы могли бы просто объединить все три trв один.
ilkkachu
Очень верно о некоторых не буквенно-цифровых символах. Однако все, что с ними связано, становится на порядок более сложным и менее читаемым, поэтому, если только нет действительно веской причины, чтобы они были в вашем фиде данных, и это указано в вопросе, я предполагаю, что они отфильтрованы до того, как мы сюда попали. Всегда должно быть ваше явное предостережение. Я считаю, что эти конвейеры проще, например, в целях отладки, чем шарик printf, который либо работает отлично, либо взрывается у вас на лице. Здесь вы делаете одно простое изменение для каждого элемента, тестируете его, а затем добавляете еще 1.
Надрек
Виноват! Получил мои _tr_s и _sed_s полностью перепутал! Исправлено в последнем редактировании.
Надрек
1

Еще один вариант - перечислить все переменные и grep для той, которую вы хотите.

set | grep -e '^aa='

Я использую это для отладки. Я сомневаюсь, что он очень производительный, поскольку в нем перечислены все переменные.

Если бы вы делали это часто, вы могли бы сделать это такой функцией:

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

К сожалению, когда мы проверяем производительность, используя время:

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

Поэтому, если бы вы делали это очень часто, вы бы хотели версию @ F.Hauri NO FORKS, потому что она намного быстрее.

xer0x
источник