Существует ли простой способ работы с bash-словарями через jq? [закрыто]

Вопрос или проблема

Я ищу простой способ создания постоянных ассоциативных массивов bash.

Я прочитал о том, как использовать declare и его опции для ручного чтения и записи ассоциативных массивов для постоянного хранения без jq.

Ответ определенно содержится в объемистых справочных файлах jq, которые ясно демонстрируют огромные возможности формата JSON. По своей лени, я ищу очень небольшую специализированную часть руководства по jq, которая четко показывает простой ответ, касающийся только хранения, чтения и редактирования ассоциативных массивов bash с использованием jq.

Чтение

Чтение значения достаточно легко с помощью --raw-output, который убирает кавычки js в выводе и // empty, который убирает вывод nullfalse)…

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, этот подход является наиболее простым и эффективным способом взаимодействия между ними. Позаботьтесь о том, чтобы тщательно проверить ваши ключи и значения перед их сериализацией и десериализацией, а также позаботьтесь о формате ввода-вывода, чтобы избежать неожиданных ошибок.

Оцените материал
Добавить комментарий

Капча загружается...