jshn: библиотека для разбора и генерации JSON в shell скриптах
jshn (JSON SHell Notation), небольшая утилита и библиотека для shell скриптов для разбора и генерации JSON данных.
Shell скрипты (ash, bash, zsh) не имеют встроенных функций для работы с JSON или другими иерархическими структурами, поэтому OpenWrt предоставляет библиотеку /usr/share/libubox/jshn.sh из пакета libubox.
Сначала вам нужно включить его в свой скрипт и затем вы можете вызвать её функции:
#!/bin/sh # source jshn shell library . /usr/share/libubox/jshn.sh # генерация данных json json_init json_add_int "code" "0" json_add_string "msg" "Hello, world!" json_add_object "test" json_add_int "testdata" "1" json_close_object MSG_JSON=`json_dump` # Переменная MSG_JSON теперь содержит: { "code": 0, "msg": "Hello, world!", "test": { "testdata": 1 } } # разбор json данных из переменной MSG_JSON local var1 code msg # объявляем локальные переменные куда загружать данные json_load "$MSG_JSON" json_select test # войти в объект внутри поля "test" json_get_var var1 testdata # сначала имя переменной "var1", затем JSON поле "testdata" json_select .. # вернуться на верхний уровень # загрузить поле "code" в соответствующую переменную code, и поле "msg" в переменную msg json_get_vars code msg echo "code: $code, msg: $msg, testdata: $var1"
Другой пример:
#!/bin/sh # импортировать библиотеку jshn . /usr/share/libubox/jshn.sh # инициализировать структуру выходного JSON json_init # добавить булевое поле json_add_boolean foo 0 # добавить целочисленное поле json_add_int code 123 # добавить строку, экранизировать спец символы json_add_string result "Some complex string\n with newlines\n and even command output: $(date)" # добавить массив с тремя целыми числами json_add_array alist json_add_int "" 1 json_add_int "" 2 json_add_int "" 3 json_close_array # добавить объект (словарь) json_add_object adict json_add_string foo bar json_add_string bar baz json_close_object # построить JSON объект и напечатать его в stdout json_dump # выведет что-то наподобие: # { "foo": false, "code": 123, "result": "Some complex string\\n with newlines\\n and even command output: Fri Jul 13 07:11:39 CEST 2018", "alist": [ 1, 2, 3 ], "adict": { "foo": "bar", "bar": "baz" } }
Примеры посложнее
Пример рабзбора файла
Данно файл с таким содержимым:
- /data1.json
{ "status": 200, "msg": "ok", "result": { "number": "99", "mac": "d85d4c9c2f1a", "last_seen": 1363777473407 } }
Скрипт чтобы разпарсить его:
#!/bin/sh . /usr/share/libubox/jshn.sh json_init json_load_file /data2.txt ## Загрузить JSON из файла ## json_get_var <local_var> <json_var> json_get_var status status ## Получить значение статуса внутри строки JSON (т.е. MSG) в локальную переменную "status" json_get_var msg msg json_select result ## Выбрать объект "result" из JSON строки (т.е. MSG) json_get_var number number json_get_var mac mac json_get_var last_seen last_seen
Пример разбора массивов
Данно файл с таким содержимым:
- /data2.json
{ "ip_addrs": { "lan": ["192.168.0.100","192.168.0.101","192.168.0.200"] } }
Скрипт для его разбора:
#!/bin/sh . /usr/share/libubox/jshn.sh json_init json_load_file /data2.json json_select "ip_addrs" if json_is_a lan array then json_select lan idx=1 # учтите что позиция элементов массива начинается с 1, а не с 0 while json_is_a ${idx} string ## перебрать данные из объекта "lan" do json_get_var ip_addr $idx echo "$ip_addr" idx=$(( idx + 1 )) done fi
Разбор списка объектов
Пример будет загружать цены на электроэнергию по часам для Финляндии в JSON и разбирать её. Цены в JSON выглядят как
- prices.json
{ "prices": [ { "date": "2024-08-03T11:00:00.000Z", "value": 6.41 }, { "date": "2024-08-03T12:00:00.000Z", "value": 4.1 }, ] }
Скрипт для его разбора:
#!/bin/sh set -e . /usr/share/libubox/jshn.sh date=$(date -u +%Y-%m-%dT%H:00:00.000Z) # скачать цены в JSON с помощью wget в тихом режиме (quiet mode) с выводом в stdout который затем будет сохранён в переменную PRICES_JSON PRICES_JSON=$(wget -qO - "https://sahkotin.fi/prices?start=$date") exit_status=$? # проверить код ошибки (exit code): если была ошибка то выйти if [ $exit_status -ne 0 ]; then >&2 echo "error $exit_status" exit $exit_status fi json_load "$PRICES_JSON" json_select "prices" idx=1 # учтите что позиция элементов массива начинается с 1, а не с 0 # итерироваться по данным внутри массива "price" пока элементы являются объектами while json_is_a $idx object do json_select $idx # теперь разобрать {"date": "2024-08-04T21:00:00.000Z", "value": 22.58} json_get_var price_date "date" echo "price_date: $price_date" json_get_var price_value "value" echo "price_value: $price_value" idx=$(( idx + 1 )) json_select .. # вернуться к верхнему уровню в массив цен done echo "Total parsed $idx"
json_for_each_item
Функция полезна для итерации через различные элементы массива или объекта. Переданная функция обратного вызова (callback) вызывается для каждого элемента, для которого передаётся его значение, ключ и дополнительные аргументы пользователя (если надо). Для типов полей, отличных от массива или объекта, функция обратного вызова вызывается с полученным значением.
#!/bin/sh . /usr/share/libubox/jshn.sh json_load_file /data2.json dump_item() { echo "item: $1 '$2'" } json_for_each_item "dump_item" "ip_addrs"
json_get_values
Для получения всех значений массива используйте json_get_values:
#!/bin/sh . /usr/share/libubox/jshn.sh json_load '{"alist":[1,2]}' json_get_values values "alist" echo "${values}" #=> 1 2 # напечатать через запятую echo "${values// /, }" #=> 1, 2
Получить все поля в переменные
Получить все поля и объявить для них переменные:
json_load '{"params":{"id": 1, "name": "Alice"}}' json_select "params" json_get_keys keys for key in $keys do json_get_var "$key" "$key" done echo "$id" #=> 1 echo "$name" #=> Alice
Утилита jshn
Внутри эта библиотека /usr/share/libubox/jshn.sh это просто обвёртка над командой /usr/bin/jshn которая и делает сам синтаксический разбор (парсинг).
Справка программы:
root@OpenWrt:/# jshn Usage: jshn [-n] [-i] -r <message>|-R <file>|-o <file>|-p <prefix>|-w
Options:
-r <message>parse from string<message>: called fromjson_load()-R <file>parse from file<file>: called fromjson_load_file()-wwrite the constructed object to stdout: called fromjson_dump()-o <file>write to file<file>-p <prefix>set prefix-nno newlines \n on formatting-iindent nested objects on formatting
Вы можете вызвать её напрямую, н.п. jshn -R /etc/board.json выведет:
json_init; json_add_object 'model'; json_add_string 'id' 'innotek-gmbh-virtualbox'; json_add_string 'name' 'innotek GmbH VirtualBox'; json_close_object; json_add_object 'network'; json_add_object 'lan'; json_add_string 'ifname' 'eth0'; json_add_string 'protocol' 'static'; json_close_object; json_add_object 'wan'; json_add_string 'ifname' 'eth1'; json_add_string 'protocol' 'dhcp'; json_close_object; json_close_object;
Затем этот вывод вычисляется внутри shell скрипта для создания в структуре памяти как в файле.
Если вы создали объект, как:
json_init; json_add_string 'username' 'root'; json_dump;
Затем внутри он вызовет jshn -w с json объектом переданном через несколько переменных окружения (env) например
root@OpenWrt:/# JSON_CUR=J_V T_J_V_username=string K_J_V=username J_V_username=root jshn -w
{ "username": "root" }
Здесь “J_V” означает “JSON value”:
JSON_CURозначает имя переменной с корневым объектом для форматированияK_J_V- это имя ключа, напримерusernameT_J_V_username- тип поляusernameJ_V_username=root- значение поляusername, т.е.root
Другие примеры
Получить состояние сетевого моста
У OpenWrt есть команда devstatus для проверки состояния сетевых устройств. Н.п. devstatus br-lan выведет:
{ "external": false, "present": true, "type": "bridge", "up": true, "carrier": true, "bridge-members": [ "eth0.1", "wlan0" ], "mtu": 1500, "mtu6": 1500, "macaddr": "84:16:f9:9b:e0:7a", "txqueuelen": 1000, "ipv6": true, "promisc": false, "rpfilter": 0, "acceptlocal": false, "igmpversion": 0, "mldversion": 0, "neigh4reachabletime": 30000, "neigh6reachabletime": 30000, "neigh4gcstaletime": 60, "neigh6gcstaletime": 60, "neigh4locktime": 100, "dadtransmits": 1, "multicast": true, "sendredirects": true, "statistics": { "collisions": 0, "rx_frame_errors": 0, "tx_compressed": 0, "multicast": 0, "rx_length_errors": 0, "tx_dropped": 0, "rx_bytes": 10609108786, "rx_missed_errors": 0, "tx_errors": 0, "rx_compressed": 0, "rx_over_errors": 0, "tx_fifo_errors": 0, "rx_crc_errors": 0, "rx_packets": 39594607, "tx_heartbeat_errors": 0, "rx_dropped": 0, "tx_aborted_errors": 0, "tx_packets": 91154927, "rx_errors": 0, "tx_bytes": 121051584071, "tx_window_errors": 0, "rx_fifo_errors": 0, "tx_carrier_errors": 0 } }
Вы можете посмотреть исходный код команды devstatus и увидеть что внутри она делает вызов ubus и использует jshn для форматирования вывода:
cat /sbin/devstatus #!/bin/sh . /usr/share/libubox/jshn.sh DEVICE="$1" [ -n "$DEVICE" ] || { echo "Usage: $0 <device>" exit 1 } json_init json_add_string name "$DEVICE" ubus call network.device status "$(json_dump)"
Проверить есть ли связь с помощью devstatus и jshn
#!/bin/sh . /usr/share/libubox/jshn.sh WANDEV="$(uci get network.wan.ifname)" json_load "$(devstatus $WANDEV)" json_get_var var1 speed json_get_var var2 link echo "Speed: $var1" echo "Link: $var2"
Дополнительные инструменты разбора JSON
jsonfilter
Утилита jsonfilter включена в OpenWrt. Чтобы вывести справку выполните jsonfilter --help.
Структура команды:
jsonfilter [-a] [-i <file> | -s "json..."] {-t <pattern> | -e <pattern>}
-qQuiet, no errors are printed-aImplicitly treat input as array, useful for JSON logs-i pathSpecify a JSON file to parse-s “json”Specify a JSON string to parse-l limitSpecify max number of results to show-F separatorSpecify a field separator when using export-t <pattern>Print the type of values matched by pattern-e <pattern>Print the values matched by pattern-e VAR=<pat>Serialize matched value for shelleval
Шаблоны
Patterns are JsonPath.
This tool implements $, @, [], * and the union operator ,
plus the usual expressions and literals.
It does not support the recursive child search operator .. or
the ?() and () filter expressions as those would require a
complete JavaScript engine to support them.
Examples
Display the first IPv4 address on lan:
ifstatus lan | jsonfilter -e '@["ipv4-address"][0].address'
Extract the release string from the board information:
ubus call system board | jsonfilter -e '@.release.description'
Find all interfaces which are up:
ubus call network.interface dump | \ jsonfilter -e '@.interface[@.up=true].interface'
Export br-lan traffic counters for shell eval:
devstatus br-lan | jsonfilter -e 'RX=@.statistics.rx_bytes' \ -e 'TX=@.statistics.tx_bytes'
Пример использования:
ubus call network.wireless status | jsonfilter -e '@[*]' | jsonfilter -a -e '@[1]'
Первый вызов jsonfilter построчно выводит объекты JSON радиопередатчиков, второй вызов затем принимает эти строки с использованием флага -a чтобы рассматривать их как массив, это позволяет вам выбрать первое или второе радио независимо от их названия.
jq
jq — гибкий процессор JSON командной строки, который очень популярен для скриптования.
Он не установлен по умолчанию в OpenWRT, потому что слишком большой (более 200Kb) поэтому используйте opkg update; opkg install jq.
По умолчанию он просто раскрашивает вывод, например cat /etc/board.json | jq
Смотрите также
- Awesome JSON список JSON библиотек.