Вопрос или проблема
Сохранение и извлечение ассоциативных массивов bash через declare
является зашифрованным. Мне не нужен сам bash ассоциативный массив
, мне просто нужна его функция, и я хочу, чтобы она была постоянной (ключи/значения хранились в файле).
JSON
– это красивый формат хранения данных, а jq
– это высоко универсальный инструмент для манипуляции с JSON
. Ответы на мои вопросы зарыты глубоко в справочных файлах jq
.
Из-за лени я задал этот вопрос, надеясь на небольшую меру знаний jq
, необходимых для взаимодействия jq
с bash
для цели хранения ассоциативного массива (словаря). Ответ будет состоять из трех однострочных команд bash, которые выполняют функции словаря:
- Однострочная команда bash, которая читает значение по ключу из внешнего хранимого словаря через
jq
. - Однострочная команда bash, которая записывает значение по ключу во внешний хранимый словарь через
jq
. - Однострочная команда bash, которая удаляет значение по ключу из внешнего хранимого словаря через
jq
.
По сути, я хочу мощность хранения словаря jq
в bash.
Ассоциативные массивы bash имеют ключи в виде непустых последовательностей ненулевых байтов и значения в виде последовательностей ненулевых байтов, в то время как JSON-объекты имеют ключи в виде последовательностей символов, закодированных в UTF-8 (включая нулевой символ), и значения в виде этих же строк или любого другого типа JSON.
Ассоциативный массив bash не может представлять JSON-объект, если его ключи содержат нулевые символы или пустые, или значения являются нестроковыми, а JSON-объект не может представлять ассоциативный массив bash, если ключи содержат последовательности байтов, которые не могут быть декодированы в текст (что касается значений, вы могли бы рассмотреть кодирование ([key]=$'AB\x80')
ассоциативного массива bash как {"key":[65,66,128]}
, я думаю).
Вы могли бы использовать JSON, закодированный в кодировке символов, отличной от UTF-8 (хотя это не рекомендуется в более поздних версиях JSON RFC), чтобы иметь возможность хранить произвольные последовательности байтов, но jq
сам не может с этим справиться.
Таким образом, в целом, JSON не является наилучшим выбором здесь, особенно при использовании jq
для его обработки.
Bash также является плохим выбором оболочки для работы с ассоциативными массивами, поскольку, помимо тех ограничений, что ключи не могут быть пустыми, ключи и значения не могут содержать нулевые байты, это также затрудняет заполнение ассоциативных массивов в целом. zsh
не имеет этих ограничений и позволяет заполнять ассоциативный массив, предоставляя список ключей и значений, чередуя их, как в многих других языках программирования.
Но тот факт, что эти ключи/значения не могут содержать нули, на самом деле облегчает их передачу в утилиту кодирования JSON, так как вы можете передавать их, разделяя нулевыми байтами (или как аргументы, но это будет ограничено размером аргументов системного вызова execve()
большинства систем).
Например, здесь мы используем модуль JSON::PP
Perl, чтобы не устанавливать дополнительное программное обеспечение (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
)
Эти команды также должны работать с ksh93
¹, а также с zsh
, при условии, что вы измените "${!assoc[@]}"
на "${(k@)assoc}"
для списка ключей, и что ключи/значения не содержат нулей.
Пример с:
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" )
Для эквивалентов этих команд perl
в jq
(но ограниченных ассоциативными массивами bash, чьи ключи и значения являются действительным текстом закодированным в UTF-8):
Чтобы закодировать в JSON из списков ключей и значений с разделителем NULL, как выходит из команды print0
выше:
jq -Rcs '
split("\u0000")[0:-1] as $x |
$x | (length/2) as $l |
[
range(0;$l) | {
($x[.]): $x[.+$l]
}
] | add + {}'
Чтобы декодировать из JSON, производя чередующиеся ключи и значения, разделенные NUL, чтобы их можно было прочитать с помощью того while
цикла выше:
jq --raw-output0 'to_entries[][]'
¹ ksh93 была первой оболочкой, которая добавила поддержку ассоциативных массивов в 1993 году и откуда bash заимствовал вдохновение для своих собственных ассоциативных массивов 15 лет спустя (и на 10 лет позже zsh), она также не поддерживает нули в ключах/значениях, но поддерживает пустые ключи, а значения не ограничены строками; в экспериментальной встроенной поддержке было поддержано дампирование ее объектов как JSON в бета-версии ksh93v, но это было abandon в ksh2020 (также abandon) и текущие поддерживаемые версии ksh93 происходят от ksh93u+.
Эти ответы используют echo .... > A для создания примера JSON-файла (ассоциативного массива), содержащего ключи Z
и KEY
, значения которых zee
и VALUE
соответственно.
Чтение
Чтение значения по ключу через --raw-output
удаляет стиль кавычек jq в выводе, в то время как // empty
удаляет вывод jq null
(и false
)...
echo '{"Z":"zee","KEY":"VALUE"}' > A
VALUE=$(jq --raw-output '.KEY // empty' A)
Используйте --exit-status
, чтобы различать между пустым значением строки и не найденным, что нормальный вывод jq называет null
...
echo '{"Z":"zee","KEY":"VALUE"}' > A
VALUE=$(jq --exit-status --raw-output '.X' A)
Присваивает null
переменной $VALUE
и 1
переменной $?
потому что ключ X
не был найден.
Запись
Передача значений через jq --arg NAME VALUE
заставляет jq
принимать любое сохраняемое bash значение VALUE
как именованный аргумент командной строки. Присвоение jq
ссылается на VALUE
, переданное в командной строке через $ARGS.named.NAME
(и преобразуется в строковое представление JSON
)...
echo '{"Z":"zee","KEY":"VALUE"}' > A
VALUE=$'Это \t значение \n содержит "кавычки" и еще \\'
sponge A < <(jq --arg NAME "$VALUE" '."KEY"=$ARGS.named.NAME' A)
Передача значений через --rawfile NAME FILE
приводит к тому, что jq
присваивает содержимое FILE
NAME
, чтобы это значение можно было использовать в вводе jq
через ."KEY"=$NAME
(и преобразуется в строковое представление JSON
)......
echo '{"Z":"zee","KEY":"VALUE"}' > A
echo $'Это \t значение \n содержит "кавычки" и больше \\' > F
sponge A < <(jq --rawfile NAME F '."KEY"=$NAME' A)
Удаление
Удаление значения по ключу очень просто...
echo '{"Z":"zee","KEY":"VALUE"}' > A
sponge A < <(jq 'del(.KEY)' A)
Комментарий
Примеры здесь используют форму 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 и jq
Bash предоставляет базовые структуры данных, такие как ассоциативные массивы, которые идеально подходят для хранения пар ключ-значение. Однако, для достижения долговременного хранения этих массивов с возможностью управления данными, стоит рассмотреть использование формата JSON в сочетании с утилитой jq
. JSON является удобным форматом для хранения и обмена данными, а jq
— мощным инструментом для их манипуляции.
Основные проблемы и возможности
Ассоциативные массивы в Bash имеют некоторые ограничения, такие как невозможность использования пустых ключей или значений, а также ограничения по содержимому ключей. Формат JSON, с другой стороны, свободен от этих ограничений, позволяя сохранять более сложные структуры данных. В этом контексте jq
будет служить связующим звеном между Bash и JSON, обеспечивая простоту манипуляций с данными.
Примеры взаимодействия с JSON через jq
Ниже представлены три основы операций с ассоциативными массивами, реализованными в формате JSON с использованием jq
:
1. Чтение значения по ключу из файла JSON
Чтобы извлечь значение по ключу, можно использовать следующее выражение:
VALUE=$(jq --raw-output '.KEY // empty' A)
В этом примере A
— это файл JSON, где вы храните ваши данные. Если ключ KEY
существует, он будет извлечен, иначе будет возвращена пустая строка.
2. Запись значения по ключу в файл JSON
Для добавления или обновления значения по ключу в JSON-файл используйте следующую конструкцию:
sponge A < <(jq --arg NAME "$VALUE" '."KEY"=$ARGS.named.NAME' A)
Здесь sponge
используется для замены данных в файле A
, содержимое которого обновляется на основе нового значения переменной $VALUE
. Этот подход гарантирует, что файл будет перезаписан только в случае успешного выполнения команды jq
.
3. Удаление значения по ключу из файла JSON
Для удаления значения из JSON-файла можно выполнить следующее:
sponge A < <(jq 'del(.KEY)' A)
Команда del
позволяет удалить заданный ключ, обеспечивая актуальность данных в файле.
Заключение
Использование JSON как механизма хранения для ассоциативных массивов в Bash предоставляет множество преимуществ, включая возможность работы с более сложными структурами данных и использование существующих инструментов для манипуляции ими. В сочетании с jq
, который обеспечивает мощные возможности для обработки JSON, вы получаете гибкое и надежное решение для хранения и управления данными.
Таким образом, переход на этот метод хранения позволит легко масштабировать и поддерживать ваши данные в системах, где взаимосвязь и доступность информации имеют критическое значение.