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"

Функция полезна для итерации через различные элементы массива или объекта. Переданная функция обратного вызова (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:

#!/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

Внутри эта библиотека /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 from json_load()
  • -R <file> parse from file <file>: called from json_load_file()
  • -w write the constructed object to stdout: called from json_dump()
  • -o <file> write to file <file>
  • -p <prefix> set prefix
  • -n no newlines \n on formatting
  • -i indent 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 - это имя ключа, например username
  • T_J_V_username - тип поля username
  • J_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)"
#!/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"

Утилита jsonfilter включена в OpenWrt. Чтобы вывести справку выполните jsonfilter --help.

Структура команды:

jsonfilter [-a] [-i <file> | -s "json..."] {-t <pattern> | -e <pattern>}
  • -q Quiet, no errors are printed
  • -a Implicitly treat input as array, useful for JSON logs
  • -i path Specify a JSON file to parse
  • -s “json” Specify a JSON string to parse
  • -l limit Specify max number of results to show
  • -F separator Specify 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 shell eval
Шаблоны

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 — гибкий процессор JSON командной строки, который очень популярен для скриптования. Он не установлен по умолчанию в OpenWRT, потому что слишком большой (более 200Kb) поэтому используйте opkg update; opkg install jq.

По умолчанию он просто раскрашивает вывод, например cat /etc/board.json | jq

  • Last modified: 2025/02/01 18:05
  • by stokito