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

This website uses cookies. By using the website, you agree with storing cookies on your computer. Also you acknowledge that you have read and understand our Privacy Policy. If you do not agree leave the website.More information about cookies
  • Last modified: 2025/02/01 18:05
  • by stokito