- Вопрос или проблема
- 2 fork
- 3 forks
- No fork
- Сравнение времени выполнения
- Замечание
- Более устойчивая и подробная информация на StackOverflow
- Ответ или решение
- Как вывести ассоциативный массив BASH без использования цикла?
- Использование declare -p
- Практическое использование printf и paste
- Решение без fork
- Вывод в BASH 5.1
- Заключение
Вопрос или проблема
Существует ли способ вывести на печать весь массив ([ключ]=значение) без перебора всех элементов?
Предположим, я создал массив с некоторыми элементами:
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%% )*}"
Есть ли более чистый способ получить или напечатать только часть вывода в формате ключ=значение?
Мне кажется, вы задаете два разных вопроса.
Есть ли способ заставить bash напечатать эту информацию без использования цикла?
Да, но они не так хороши, как просто использование цикла.
Есть ли более чистый способ получить или напечатать только часть вывода в формате ключ=значение?
Да, цикл for
. Он имеет преимущества в том, что не требует внешних программ, прост в использовании и позволяет легко контролировать формат вывода без неожиданных сюрпризов.
Любое решение, пытающееся обработать вывод команды declare -p
(typeset -p
), должно учитывать а) возможность того, что сами переменные содержат круглые или квадратные скобки, б) кавычки, которые declare -p
должен добавить, чтобы сделать его вывод допустимым вводом для shell.
Например, ваша подстановка 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
проще выбрать формат вывода, который вам нравится:
# без кавычек
$ for x in "${!array[@]}"; do printf "[%s]=%s\n" "$x" "${array[$x]}" ; done
[def]="foo bar"
[abc]='foobar'
# с кавычками
$ for x in "${!array[@]}"; do printf "[%q]=%q\n" "$x" "${array[$x]}" ; done
[def]=\"foo\ bar\"
[abc]=\'foobar\'
После этого также просто изменить формат вывода (удалить скобки вокруг ключа, выводить все пары ключ/значение в одну строку и т. д.). Если вам нужны кавычки для чего-то, кроме самого shell, вам все равно придется делать это самостоятельно, но, по крайней мере, у вас будут исходные данные для работы. (Если у вас есть переводы строки в ключах или значениях, вам, вероятно, понадобятся кавычки.)
С современным 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
declare -p array
declare -A array='([a2]="2" [a1]="1" [zz]="Hello World" [b1]="bbb" [f50]="abcd" )'
2 fork
Может быть, это:
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 forks
или это:
paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}")
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb
No fork
по сравнению с
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
Однако это утверждение неверно, если массив становится большим; уменьшение числа fork эффективно для небольших процессов, но использование специализированных инструментов более эффективно для более крупных процессов.
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
Замечание
Так как оба (forked) решения используют выравнивание, ни одно из них не будет работать, если какая-либо переменная содержит перевод строки. В этом случае единственным способом является цикл for
.
Более устойчивая и подробная информация на StackOverflow
Без циклов и fork!
printf -v fmtStr '[%s]=%%q\\n' "${!array[@]}";printf "$fmtStr" "${array[@]}"
Bash 5.1 позволяет очень простой способ отображения ассоциативных массивов с использованием значения K
, как в ${arr[@]@K}
:
$ declare -A arr
$ arr=(k1 v1 k2 v2)
$ printf "%s\n" "${arr[@]@K}"
k1 "v1" k2 "v2"
Из документа описания Bash 5.1:
hh. Новое преобразование параметра `K` для отображения ассоциативных массивов как пар ключ-значение.
Это хорошо объяснено в Руководстве по справке Bash → 3.5.3 Расширение параметров шелла:
${parameter@operator}
K
Создает возможно закавыченную версию значения параметра, но выводит значения индексируемых и ассоциативных массивов как последовательность пар ключ-значение с кавычками (смотрите Массивы).
Если вы ищете shell с лучшей поддержкой ассоциативных массивов, попробуйте 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 использует гораздо более разумный и удобный синтаксис определения массива, чем ksh93
(скопированный 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})
Поскольку 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. Измените их, чтобы удовлетворить вкусы для красивой печати.
Еще один вариант – перечислить все переменные и использовать grep
для поиска нужной.
set | grep -e '^aa="
Я использую это для отладки. Я сомневаюсь, что это очень производительно, так как это перечисляет все переменные.
Если бы вы делали это часто, вы могли бы создать функцию, например, такую:
aap() { set | grep -e "^$1="; }
К сожалению, когда мы проверяем производительность с помощью time:
$ time aap aa
aa=([0]="abc")
.
real 0m0.014s
user 0m0.003s
sys 0m0.006s
Поэтому, если бы вы делали это очень часто, вы бы хотели использовать версию @F.Hauri’s NO FORKS, потому что она намного быстрее.
.
Ответ или решение
Как вывести ассоциативный массив BASH без использования цикла?
Ассоциативные массивы в BASH — чрезвычайно полезный инструмент для работы с данными в формате ключ-значение. Однако часто возникает вопрос: как можно вывести ассоциативный массив целиком, избегая циклов? Давайте рассмотрим несколько решений этой задачи, принимая во внимание производительность и удобство.
Использование declare -p
Одним из самых простых способов вывести содержимое ассоциативного массива является использование команды declare -p
. Этот метод позволяет вывести массив в формате, пригодном для повторного использования в BASH:
declare -p array
Однако вывод будет содержать метаинформацию и служебные символы вроде скобок, которые могут быть избыточными для ваших нужд. Для более чистой записи можно обработать вывод, используя sed
и tr
:
typeset -p array | sed 's/^.*(//' | tr -d ")'\"" | tr "[" "\n" | sed 's/]=/ = /'
Практическое использование printf
и paste
Хотя declare
и предоставляет готовое решение, для более детального контроля над форматом вывода идеален printf
. Этот подход особенно эффективен при небольших объемах данных.
paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}")
Этот метод дополнительно позволяет красиво выровнять ключи и значения без использования циклов.
Решение без fork
Если производительность критична, и необходимо избегать вызова внешних утилит, лучше всего подойдут встроенные средства BASH:
for i in "${!array[@]}"; do printf "%s=%s\n" "$i" "${array[$i]}"; done
Пусть это и цикл, но его использование напрямую в скрипте BASH будет более оптимальным, чем использование дополнительных команд, особенно в сценариях с большими массивами.
Вывод в BASH 5.1
С недавними обновлениями BASH, начиная с версии 5.1, стало проще работать с ассоциативными массивами. Можно использовать модификатор K
для вывода в формате пара ключ-значение:
printf "%s\n" "${array[@]@K}"
Этот метод выводит массив сразу в нужном формате, сохраняет строгий порядок ключ-значение и поддерживает более наглядное отображение.
Заключение
В зависимости от специфики ваших задач, вы можете выбрать оптимальный метод работы с ассоциативными массивами в BASH. Если важна производительность, предпочтите встроенные возможности языка. Для форматированного вывода применяйте printf
. Для упрощенной работы без циклов используйте возможности последних версий BASH. Всегда ориентируйтесь на баланс между удобством, производительностью и поддерживаемостью вашего кода.