Вопрос или проблема
Я ищу простой способ создания постоянных ассоциативных массивов bash.
Я прочитал о том, как использовать declare
и его опции для ручного чтения и записи ассоциативных массивов для постоянного хранения без jq.
Ответ определенно содержится в объемистых справочных файлах jq, которые ясно демонстрируют огромные возможности формата JSON. По своей лени, я ищу очень небольшую специализированную часть руководства по jq, которая четко показывает простой ответ, касающийся только хранения, чтения и редактирования ассоциативных массивов bash с использованием jq.
Чтение
Чтение значения достаточно легко с помощью --raw-output
, который убирает кавычки js в выводе и // empty
, который убирает вывод null
(и false
)…
echo '{"Z":"zee","V":"vee"}' > d
V=$(jq --raw-output '.V // empty' d)
Используйте --exit-status
, если хотите различать между пустой строкой и отсутствующим значением, что js называет null
…
echo '{"Z":"zee","V":"vee"}' > d
V=$(jq --exit-status --raw-output '.X' d)
Присваивает null
переменной $V
и 1
переменной $?
потому что ключ X
не найден.
Запись
Передача значения через --arg NAME VALUE
заставляет jq
принять VALUE
как аргумент командной строки и присвоить его JSON
строковое представление переменной NAME
, которое может быть использовано в вводе jq
как $ARGS.named.NAME
…
echo '{"Z":"zee","V":"vee"}' > d
V=$'This\tis a fancy\nvalue containing "quotes" and more \\'
sponge d < <(jq --arg V "$V" '."V"=$ARGS.named.V' d)
Передача значения через --rawfile NAME FILE
заставляет jq
принять содержимое FILE
и присвоить его JSON
строковое представление переменной NAME
, которое может быть использовано в вводе jq
как $NAME
…
echo '{"Z":"zee","V":"vee"}' > d
sponge d < <(jq --rawfile V <(printf 'This\tis a fancy\nvalue containing "quotes" and more \\') '."V"=$V' d)
Удаление
Удаление значения достаточно просто…
sponge d < <(jq 'del(.V)' d)
Комментарий
Примеры здесь используют форму sponge FILE < <(jq ... FILE)
для эмуляции замены FILE
на месте. Любой другой метод, такой как JSON=$(jq ...)
, также может быть использован, но jq ... FILE | sponge FILE
заставляет sponge
очистить FILE
, если происходит ошибка во время jq
(что легко может произойти из-за неправильно сформированного входа или литерала), что нельзя остановить с помощью set -e
и set -o pipefail
. С другой стороны, sponge FILE < <(jq ... FILE)
останавливает выполнение sponge
, если произошла ошибка во время генерации его ввода, оставляя FILE
нетронутым.
Ассоциативные массивы bash имеют ключи в виде непустых последовательностей ненулевых байтов и значения в виде последовательностей ненулевых байтов, в то время как объекты JSON имеют ключи в виде последовательностей символов, закодированных в UTF-8 (включая символ NULL), и значения в виде тех же строк или любого другого типа JSON.
Ассоциативный массив bash не может представлять объект JSON, если его ключи содержат символы NULL или пустые, или значения не являются строками, а объект JSON не может представлять ассоциативный массив bash, если его ключи содержат байтовые последовательности, которые нельзя декодировать в текст (для значений можно рассмотреть возможность кодирования ассоциативного массива bash ([key]=$'AB\x80')
как {"key":[65,66,128]}
, я полагаю).
Вы можете использовать JSON, закодированный в другой кодировке, отличной от UTF-8 (хотя это не рекомендуется в более поздних версиях RFC JSON), чтобы иметь возможность хранить произвольные последовательности байтов, но jq
сам с этим не справится.
Таким образом, в целом, JSON не является лучшим выбором здесь, особенно при использовании jq
для его обработки.
Bash также является плохим выбором оболочки для работы с ассоциативными массивами, так как, помимо тех ограничений, что ключи не могут быть пустыми, ключи и значения не могут содержать нулевые байты, это также затрудняет заполнение ассоциативных массивов целиком. zsh
не имеет таких ограничений и позволяет вам заполнять ассоциативный массив, предоставляя список ключей и значений, чередующихся, как во многих других языках программирования.
Но тот факт, что эти ключи/значения не могут содержать нулевые байты, на самом деле облегчает их передачу туда и обратно в утилиту кодирования JSON, так как вы можете передавать их раздельно с нулевыми байтами (или как аргументы, но это будет иметь ограничение по размеру аргументов системного вызова execve()
на большинстве систем).
Например, здесь используется модуль perl JSON::PP
, чтобы вам не пришлось устанавливать дополнительное программное обеспечение (perl
, в отличие от jq
, установлен по умолчанию на большинстве систем, и JSON::PP
является основным модулем), чтобы иметь возможность работать с не-UTF-8 JSON:
Для кодирования в JSON:
print0() { [ "$#" -eq 0 ] || printf '%s\0' "$@"; }
print0 "${!assoc[@]}" "${assoc[@]}" |
perl -MJSON::PP -MList::Util=mesh -l -0e '
chomp(@values = <>);
@keys = splice(@values, 0, @values / 2);
print JSON::PP->new->encode({mesh(\@keys, \@values)})' > file.json
Для декодирования из JSON:
typeset -A assoc=()
while IFS= read -ru3 -d '' key && IFS= read -ru3 -d '' value; do
assoc[$key]=$value
done 3< <(
perl -MJSON::PP -0777 -l0 -ne 'print for %{JSON::PP->new->decode($_)}' file.json
)
Пример с:
typeset -A assoc=(
[a]=b
[$'a\nb']=''
[$'St\xe9phane']=1
[$'St\xc3\xa9phane']=2
)
(обратите внимание, что Stéphane
закодирован как в ISO8859-1, так и в UTF-8).
Кодирование дает:
$ cat file.json
{"a\nb":"","Stéphane":"2","a":"b","St�phane":"1"}
(где �
— это то, как мой терминальный эмулятор UTF-8 представляет собой байт 0xe9, который нельзя декодировать в UTF-8).
А после декодирования:
$ typeset -p assoc
declare -A assoc=([$'St\351phane']="1" [$'a\nb']="" [Stéphane]="2" [a]="b" )
Ответ или решение
Для управления ассоциативными массивами в Bash с использованием jq
необходимо понимать, как организовать взаимодействие между ними и форматом JSON. Ваш запрос о том, существует ли простой способ интерфейса между Bash-диктами и jq
, имеет одно из лучших решений, которое поможет вам в создании, чтении и модификации ассоциативных массивов.
Чтение значений
Чтение значений из JSON-файла можно выполнить с помощью jq
и опции --raw-output
, что упрощает вывод, избавляя вас от дополнительных кавычек. Например, следующий код позволяет вам получить значение по ключу:
echo '{"Z":"zee","V":"vee"}' > d
V=$(jq --raw-output '.V // empty' d)
Если ключ отсутствует, jq
вернет null
, что можно обрабатывать с помощью флага --exit-status
:
V=$(jq --exit-status --raw-output '.X' d)
Запись значений
Для записи значений в ассоциативный массив JSON используется --arg
или --rawfile
. Это позволяет передать значение в jq
, которое будет интерпретировано как строка JSON. Пример:
V=$'This\tis a fancy\nvalue containing "quotes" and more \\'
sponge d < <(jq --arg V "$V" '."V"=$ARGS.named.V' d)
Кроме передачи значений через аргументы, вы также можете использовать --rawfile
:
sponge d < <(jq --rawfile V <(printf 'This\tis a fancy\nvalue containing "quotes" and more \\') '."V"=$V' d)
Удаление значений
Удаление ключа из JSON является простым процессом:
sponge d < <(jq 'del(.V)' d)
Проблемы с JSON и ассоциативными массивами в Bash
Важно отметить, что ключи и значения в ассоциативных массивах Bash имеют свои ограничения. JSON, в свою очередь, позволяет более широкий диапазон символов, включая нулевые символы. Это может создать проблемы при преобразовании, так как Bash не поддерживает ключи с нулевыми символами.
Альтернативный подход через Perl
Поскольку Bash имеет свои ограничения в управлении ассоциативными массивами, использование Perl может быть более эффективным решением. Модуль JSON::PP
в Perl позволяет вам легко выполнять кодирование и декодирование JSON, обходя большинство ограничений Bash:
Кодирование в JSON:
print0() { [ "$#" -eq 0 ] || printf '%s\0' "$@"; }
print0 "${!assoc[@]}" "${assoc[@]}" |
perl -MJSON::PP -MList::Util=mesh -l -0e '
chomp(@values = <STDIN>);
@keys = splice(@values, 0, @values / 2);
print JSON::PP->new->encode({mesh(\@keys, \@values)})' > file.json
Декодирование из JSON:
typeset -A assoc=()
while IFS= read -ru3 -d '' key && IFS= read -ru3 -d '' value; do
assoc[$key]=$value
done 3< <(
perl -MJSON::PP -0777 -l0 -ne 'print for %{JSON::PP->new->decode($_)}' file.json
)
Заключение
Данные методы позволяют эффективно работать с ассоциативными массивами в Bash, используя jq
и Perl. С учетом ограничений Bash и возможностей jq
, этот подход является наиболее простым и эффективным способом взаимодействия между ними. Позаботьтесь о том, чтобы тщательно проверить ваши ключи и значения перед их сериализацией и десериализацией, а также позаботьтесь о формате ввода-вывода, чтобы избежать неожиданных ошибок.