OpenWrt之shell脚本处理UCI配置
OpenWRT提供了一些列shell标准接口(config_load, config_get, config_get_bool, config_cb, option_cb, list_cb, config_foreach, config_list_foreach
,以便在shell脚本中高效地读取和处理uci配置。
这主要用于在/etc/init.d
中写启动脚本。
OpenWRT在/lib/functions.sh
文件中提供了访问uci配置的shell接口,因此在使用这些接口之间需要直接或间接(包含rc.common
)的方式包含该文件。
接下来讨论一下看一些处理uci配置的方法
加载
/lib/functions.sh
脚本中包含了加载uci配置的接口,接口信息如下所示:
...
config_load() {
[ -n "$IPKG_INSTROOT" ] && return 0
uci_load "$@"
}
...
[ -z "$IPKG_INSTROOT" ] && [ -f /lib/config/uci.sh ] && . /lib/config/uci.sh
其中uci_load
由uci工具提供(uci.sh
),而uci.sh
中有关uci_load
的定义如下:
CONFIG_APPEND=
uci_load() {
local PACKAGE="$1"
local DATA
local RET
local VAR
_C=0
if [ -z "$CONFIG_APPEND" ]; then
for VAR in $CONFIG_LIST_STATE; do
export ${NO_EXPORT:+-n} CONFIG_${VAR}=
export ${NO_EXPORT:+-n} CONFIG_${VAR}_LENGTH=
done
export ${NO_EXPORT:+-n} CONFIG_LIST_STATE=
export ${NO_EXPORT:+-n} CONFIG_SECTIONS=
export ${NO_EXPORT:+-n} CONFIG_NUM_SECTIONS=0
export ${NO_EXPORT:+-n} CONFIG_SECTION=
fi
DATA="$(/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} ${LOAD_STATE:+-P /var/state} -S -n export "$PACKAGE" 2>/dev/null)"
RET="$?"
[ "$RET" != 0 -o -z "$DATA" ] || eval "$DATA"
unset DATA
${CONFIG_SECTION:+config_cb}
return "$RET"
}
因此如果要在脚本中加载uci配置首先需要包含/lib/functions.sh
脚本,可以在使用前通过下面的一行代码包含该脚本:
. /lib/functions.sh
当然如果是编写启动脚本的话,一般会以#!/bin/sh /etc/rc.common
开头,此时就不需要再额外包含/lib/functions.sh
,因为/etc/rc.common
中已经包含了/lib/functions.sh
。
以miniupnpd的启动脚本为例,/etc/init.d/miniupnpd
内容如下:
#!/bin/sh /etc/rc.common
# Copyright (C) 2006-2014 OpenWrt.org
START=94
STOP=15
USE_PROCD=1
PROG=/usr/sbin/miniupnpd
[ -x "$(command -v nft)" ] && FW="fw4" || FW="fw3"
upnpd_get_port_range() {
local var="$1"; shift
local val
config_get val "$@"
case "$val" in
[0-9]*[:-][0-9]*)
export -n -- "${var}_start=${val%%[:-]*}"
export -n -- "${var}_end=${val##*[:-]}"
;;
[0-9]*)
export -n -- "${var}_start=$val"
export -n -- "${var}_end="
;;
esac
}
conf_rule_add() {
local cfg="$1"
local action int_addr
local ext_start ext_end int_start int_end comment
config_get action "$cfg" action "deny" # allow or deny
upnpd_get_port_range "ext" "$cfg" ext_ports "0-65535" # external ports: x, x-y, x:y
config_get int_addr "$cfg" int_addr "0.0.0.0/0" # ip or network and subnet mask (internal)
upnpd_get_port_range "int" "$cfg" int_ports "0-65535" # internal ports: x, x-y, x:y or range
config_get comment "$cfg" comment "ACL" # comment
...
}
...
/etc/rc.common
内容如下:
#!/bin/sh
# Copyright (C) 2006-2012 OpenWrt.org
. $IPKG_INSTROOT/lib/functions.sh
. $IPKG_INSTROOT/lib/functions/service.sh
initscript=$1
action=${2:-help}
shift 2
start() {
return 0
}
...
既然了解了uci配置加载,那么将uci配置加载到内存之后接下来该怎么使用呢?
下面有几种方法能够从加载到内存中的uci配置中获取到对应的参数和值,分别是回调、遍历等。
回调
UCI配置在加载到内存的过程中同时也会解析,而在解析的过程中,会为每次遇到的section
、option
、list
触发回调。
可以定义3个名为config_cb()
、option_cb()
、list_cb()
的回调函数来分别处理section
、option
以及list
。
回调函数必须在调用config_load()
函数之前,引入/lib/functions.sh
之后定义。这是因为config_load()
函数加载配置时就会解析,而引入/lib/functions.sh
时会调用reset_cb()
函数,该函数会重置所有回调。
如果想要主动重置所有回调,可以调用reset_cb()
。
config_cb
在解析UCI配置过程中,每遇到section
时都会调用config_cb()
回调函数。
还有在config_load()
函数调用结束之后也会调用一次config_cb()
,而这次调用不携带任何参数。这个条件可以用来判断是否解析完成。
当遇到section
而被调用时,config_cb
会收到两个参数:
section_type
,例如config interface wan
中的interface
。section_name
,例如config interface wan
中的wan
,或者config route
中的cfg13abef
,对于匿名section
会自动生成名称。
用法:
config_cb() {
local section_type="$1"
local section_name="$2"
# commands to be run for every section
}
option_cb
与config_cb()
类似,UCI配置解析过程中,每遇到option
就会触发option_cb()
回调。
当被调用时,option_cb()
回调函数会接受两个参数:
option_name
:例如option ifname eth0
中的ifname
。option_value
:例如option ifname eth0
中的eth0
。
用法:
option_cb() {
local option_name="$1"
local option_value="$2"
# commands to be run for every option
}
在该回调中,可以通过$CONFIG_SECTION
变量来访问当前section_name
。
可以在config_cb()
函数中根据section_type
来定义option_cb()
回调函数,用以处理不同的section_type
。
list_cb
list_cb()
和上面的option_cb()
基本相同,但只有遇到list
时才会调用。
被调用时,会接收两个参数:
list_name
:例如list server 192.168.0.1
中的server
list_value
:例如list server 192.168.0.1
中的192.168.0.1
用法:
list_cb() {
local list_name="$1"
local list_value="$2"
# commands to be run for every list item
}
list
中的每个项都会触发一次list_cb()
调用。
示例
下面以/etc/config/network
为例:
root@OpenWrt:~/uci# cat /etc/config/network
config interface 'loopback'
option device 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config globals 'globals'
option ula_prefix 'fdef:6d48:4a40::/48'
config device
option name 'br-lan'
option type 'bridge'
list ports 'eth0'
config interface 'lan'
option device 'br-lan'
option proto 'dhcp'
option ipaddr '192.168.1.1'
option netmask '255.255.255.0'
option ip6assign '60'
# 实时数据,用于和旧版保持兼容
root@OpenWrt:~/uci# cat /var/state/network
network.loopback.up='1'
network.loopback.ifname='lo'
network.lan.up='1'
network.lan.ifname='br-lan'
下面的脚本会采用回调的方式读取/etc/config/network
中的配置:
#!/bin/sh
. /lib/functions.sh
config_cb() {
local section_type="$1"
local section_name="$2"
echo "section_type: $section_type"
echo "section_name: $section_name"
}
option_cb() {
local option_name="$1"
local option_value="$2"
echo "option_name=$option_name"
echo "option_value=$option_value"
}
list_cb() {
local list_name="$1"
local list_value="$2"
echo "list_name=$list_name"
echo "list_name=$list_value"
}
config_load "network"
# section_type: interface
# section_name: loopback
# option_name=device
# option_value=lo
# option_name=proto
# option_value=static
# option_name=ipaddr
# option_value=127.0.0.1
# option_name=netmask
# option_value=255.0.0.0
# option_name=up
# option_value=1
# option_name=ifname
# option_value=lo
# section_type: globals
# section_name: globals
# option_name=ula_prefix
# option_value=fdef:6d48:4a40::/48
# section_type: device
# section_name: cfg030f15
# option_name=name
# option_value=br-lan
# option_name=type
# option_value=bridge
# list_name=ports
# list_name=eth0
# section_type: interface
# section_name: lan
# option_name=device
# option_value=br-lan
# option_name=proto
# option_value=dhcp
# option_name=ipaddr
# option_value=192.168.1.1
# option_name=netmask
# option_value=255.255.255.0
# option_name=ip6assign
# option_value=60
# option_name=up
# option_value=1
# option_name=ifname
# option_value=br-lan
# section_type:
# section_name:
从最后的section_type
和section_name
为空来看,这说明在解析完成之后最后会调用一次config_cb()
,和上面的说明一致。
下面的示例展示了在config_cb()
内部定义option_cb()
,然后对section_type
为interface
单独处理,同时在option_cb()
中通过$CONFIG_SECTION
访问当前section_name
。
#!/bin/sh
. /lib/functions.sh
config_cb() {
local section_type="$1"
local section_name="$2"
echo "section_type: $section_type"
echo "section_name: $section_name"
if [ "$section_type" = "interface" ]; then
echo "internal optioncb"
echo "CONFIG_SECTION=$CONFIG_SECTION"
option_cb() {
local option_name="$1"
local option_value="$2"
echo "option_name=$option_name"
echo "option_value=$option_value"
}
else
echo "internal optioncb"
echo "CONFIG_SECTION=$CONFIG_SECTION"
option_cb() {
local option_name="$1"
local option_value="$2"
echo "option_name=$option_name"
}
fi
}
list_cb() {
local list_name="$1"
local list_value="$2"
echo "list_name=$list_name"
echo "list_name=$list_value"
}
config_load "network"
# section_type: interface
# section_name: loopback
# internal optioncb
# CONFIG_SECTION=loopback
# option_name=device
# option_value=lo
# option_name=proto
# option_value=static
# option_name=ipaddr
# option_value=127.0.0.1
# option_name=netmask
# option_value=255.0.0.0
# option_name=up
# option_value=1
# option_name=ifname
# option_value=lo
# section_type: globals
# section_name: globals
# internal optioncb
# CONFIG_SECTION=globals
# option_name=ula_prefix
# section_type: device
# section_name: cfg030f15
# internal optioncb
# CONFIG_SECTION=cfg030f15
# option_name=name
# option_name=type
# list_name=ports
# list_name=eth0
# section_type: interface
# section_name: lan
# internal optioncb
# CONFIG_SECTION=lan
# option_name=device
# option_value=br-lan
# option_name=proto
# option_value=dhcp
# option_name=ipaddr
# option_value=192.168.1.1
# option_name=netmask
# option_value=255.255.255.0
# option_name=ip6assign
# option_value=60
# option_name=up
# option_value=1
# option_name=ifname
# option_value=br-lan
# section_type:
# section_name:
# internal optioncb
# CONFIG_SECTION=lan
遍历(config_foreach
)
上面的回调函数是UCI配置在加载(config_load()
)的过程中调用的。而config_foreach()
函数是在config_load
调用结束之后主动调用,用于遍历UCI配置中的section
。
调用config_foreach()
函数至少携带1个参数:
handle_function
:为遇到的每个section
触发的回调函数。section_type
:可选参数,只遍历section_type
指定的section
。additional_arg
:可选参数,传递给处理函数的自定义参数。
用法:
handle_interface() {
local section_type="$1"
local custom_arg1="$2"
local custom_arg2="$3"
# run commands for every interface section
}
config_load network
config_foreach handle_interface interface test1 test2
config_foreach()
函数不会考虑处理函数的返回值,即使中间出错也会继续向下调用。
其次,在处理函数中可以通过调用config_get()
和config_set()
函数来获取和设置option
。
示例如下:
#!/bin/sh
. /lib/functions.sh
handle_interface() {
local section_name="$1"
local custom_arg1="$2"
local custom_arg2="$3"
# run commands for every interface section
echo "section_name=$section_name"
echo "custom_arg1=$custom_arg1"
echo "custom_arg2=$custom_arg2"
local ifname=
config_get ifname "$section_name" ifname
echo "ifname=$ifname"
}
config_load network
config_foreach handle_interface interface test1 test2
# section_name=loopback
# custom_arg1=test1
# custom_arg2=test2
# ifname=lo
# section_name=lan
# custom_arg1=test1
# custom_arg2=test2
# ifname=br-lan
config_foreach
函数源码如下:
config_foreach() {
local ___function="$1" # 回调函数
[ "$#" -ge 1 ] && shift # $# 表示传入参数个数,如果>1则左移,抛弃第1个参数,第2个参数变为$1
local ___type="$1" # 保存section_type
[ "$#" -ge 1 ] && shift # 参数继续左移,同上,这里表示如果还有参数,则为自定义参数
local section cfgtype
[ -z "$CONFIG_SECTIONS" ] && return 0 # 判断是否存在section -z 判断长度是否为0
for section in ${CONFIG_SECTIONS}; do # 遍历所有的section_name
config_get cfgtype "$section" TYPE # 获取已有的section_type
[ -n "$___type" ] && [ "x$cfgtype" != "x$___type" ] && continue # 存在传入的section_type!=已有的section_type,则跳过
eval "$___function \"\$section\" \"\$@\"" # $@表示全部参数
done
}
读取option(config_get
)
config_get
接口用来获取指定option_name
的值
config_get
接口至少携带3个参数:
valriable
:变量,用来保存获取到的option_value
。section_name
:用来指定哪个section
。option_name
:用来指定具体哪个option
default
:可选,如果option_name
不存在,则返回该值。
示例:
#!/bin/sh
. /lib/functions.sh
config_load network
config_get_ifname() {
local section_name="loopback"
local iface
# 获取loopback section中的ifname option,如果不存在则返回default-lo
config_get iface "$section_name" ifname "default-lo"
echo "iface=$iface"
}
config_get_ifname
# 输出如下:
# iface=lo
config_get()
函数源码如下:
# config_get <variable> <section> <option> [<default>]
# config_get <section> <option>
config_get() {
case "$2${3:-$1}" in # 如果$3不存在,则使用$1,以上面的示例值为loopbackifname
*[!A-Za-z0-9_]*) : ;;
*)
case "$3" in
"") eval echo "\"\${CONFIG_${1}_${2}:-\${4}}\"";;
# 下面一行等价于export -n iface=${CONFIG_loopback_ifname:-"default-lo"}
# 表示如果$CONFIG_loopback_ifname不存在取值default-lo
*) eval export ${NO_EXPORT:+-n} -- "${1}=\${CONFIG_${2}_${3}:-\${4}}";;
esac
;;
esac
}
设置option(config_set
)
config_set
用来设置option
值,可携带3个参数:
option
所在的section_name
。option_name
option_value
原型如下:
config_set() {
local section="$1"
local option="$2"
local value="$3"
export ${NO_EXPORT:+-n} "CONFIG_${section}_${option}=${value}"
}
从函数实现中就可以看出,config_set()
函数仅设置内存中的值,底层UCI配置文件中的值不会变。
如果想要修改底层配置文件中的值可以使用uci_set()
函数,定义在/lib/config/uci.sh
,函数实现如下:
uci_set() {
local PACKAGE="$1"
local CONFIG="$2"
local OPTION="$3"
local VALUE="$4"
/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} set "$PACKAGE.$CONFIG.$OPTION=$VALUE"
}
使用示例如下:
#!/bin/sh
. /lib/functions.sh
config_set_ifname() {
local section_name="loopback"
echo "CONFIG_loopback_ifname=$CONFIG_loopback_ifname"
config_set $section_name ifname "lo1"
echo "CONFIG_loopback_ifname=$CONFIG_loopback_ifname"
}
uci_set_ifname() {
local package_name="network"
local section_name="loopback"
echo "CONFIG_loopback_ifname=$CONFIG_loopback_ifname"
uci_set $package_name $section_name ifname "lo1"
uci_commit
echo "CONFIG_loopback_ifname=$CONFIG_loopback_ifname"
}
config_load network
# config_set_ifname
# uci_set_ifname
# config_set执行结果如下
# root@OpenWrt:~/uci# ./config_set_example.sh
# 内存中的值发生变化,文件中未变化
# CONFIG_loopback_ifname=lo
# CONFIG_loopback_ifname=lo1
# root@OpenWrt:~/uci# cat /etc/config/network | grep ifname
# root@OpenWrt:~/uci# cat /var/state/network | grep ifname
# network.loopback.ifname='lo' 文件中的配置就没变
# network.lan.ifname='br-lan'
#
# uci_set的执行结果如下
# root@OpenWrt:~/uci# ./config_set_example.sh
# 从执行前后CONFIG_loopback_ifname的值可以看出,内存中的值没有发生变化
# CONFIG_loopback_ifname=lo
# CONFIG_loopback_ifname=lo
# 而在uci_commit之后,文件中的值发生了变化
# root@OpenWrt:~/uci# cat /etc/config/network | grep ifname
# option ifname 'lo1'
直接访问
如果已经预先知道section_name
,则可以直接获取指定option
的值,而无需通过回调或者遍历来获取指定option
的值。当然这个前提是已经调用config_load
。
示例如下:
#!/bin/sh
. /lib/functions.sh
config_load network
config_get_ifname() {
# 事先已经知晓要获取的section_name和option_name,则在config_load之后直接获取,不需要遍历。
local section_name="loopback"
local iface
# 获取loopback section中的ifname option,如果不存在则返回default-lo
config_get iface "$section_name" ifname "default-lo"
echo "iface=$iface"
}
config_get_ifname
# 输出如下:
# iface=lo
读取lists
下面是/etc/config/network
配置文件的内容(与上文相比有更改,主要是为了增加list ports
条目)
root@OpenWrt:~/uci# cat /etc/config/network
config interface 'loopback'
option device 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config globals 'globals'
option ula_prefix 'fdf0:ee94:d198::/48'
config device
option name 'br-lan'
option type 'bridge'
list ports 'eth0'
list ports 'eth2'
list ports 'eth3'
option stp '1'
config interface 'lan'
option device 'br-lan'
option proto 'static'
option ipaddr '192.168.1.1'
option netmask '255.255.255.0'
option ip6assign '60'
config interface 'wan'
option device 'eth1'
option proto 'dhcp'
config interface 'wan6'
option device 'eth1'
option proto 'dhcpv6'
在上面的配置文件中可以看到,存在list ports xxx
配置,这些配置通过config_get()
来获取时会将所有的list_value
拼接起来,中间以空格分隔。如下面的例子所示:
#!/bin/sh
. /lib/functions.sh
handle_device() {
local section_name="$1"
echo "section_name=$section_name"
local ports
config_get ports "$section_name" ports
echo "ports=$ports"
}
config_load network
config_foreach handle_device device
# section_name=cfg030f15
# ports=eth0 eth2 eth3
那么现在就存在问题,如果list
的值中本来存在空格的话,就无法从config_get()
的结果区分开。
为了避开这个问题,OpenWrt提供了config_list_foreach()
接口,来遍历list
,该函数会为每个list
的条目触发一次。
与config_foreach()
类似,config_list_foreach()
至少需要3个参数:
list
所属的section_name
list_name
list
的处理函数list
处理函数的自定义参数
使用示例:
#!/bin/sh
. /lib/functions.sh
handle_list_ports() {
local port="$1"
local custom1="$2"
local custom2="$3"
echo "port=$1"
}
handle_interface() {
local section_name="$1"
echo "section_name=$section_name"
config_list_foreach "$section_name" ports handle_list_ports test1 test2
}
config_load network
config_foreach handle_interface device
# result of executing
# root@OpenWrt:~/uci# ./config_list_foreach_example.sh
# section_name=cfg030f15
# port=eth0
# port=eth2
# port=eth3
读取boolean
值
boolean
选项可以包含多种值来表示真或加,比如1, on, enabled, true
等都表示真。
config_get_bool()
函数可以将读取到的这些值统一转换为1或0,分别表示真或假。
config_get_bool()
的参数与config_get()
参数格式相同。
下面是config_get_bool()
函数原型:
# get_bool <value> [<default>]
get_bool() {
local _tmp="$1"
case "$_tmp" in
1|on|true|yes|enabled) _tmp=1;;
0|off|false|no|disabled) _tmp=0;;
*) _tmp="$2";;
esac
echo -n "$_tmp"
}
# config_get_bool <variable> <section> <option> [<default>]
config_get_bool() {
local _tmp
config_get _tmp "$2" "$3" "$4"
_tmp="$(get_bool "$_tmp" "$4")"
export ${NO_EXPORT:+-n} "$1=$_tmp"
}
可以看到是先通过config_get()
获取到值,然后再通过get_bool()
将其转化为1或0。
示例如下(/etc/config/network
内容见上文):
#!/bin/sh
. /lib/functions.sh
handle_interface() {
local section_name="$1"
echo "section_name=$section_name"
local stp
config_get_bool stp "$section_name" stp
echo "stp=$stp"
}
config_load network
config_foreach handle_interface device
# root@OpenWrt:~/uci# ./config_get_bool_example.sh
# section_name=cfg030f15
# stp=1
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/openwrt/openwrt%E4%B9%8Bshell%E8%84%9A%E6%9C%AC%E5%A4%84%E7%90%86uci%E9%85%8D%E7%BD%AE/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。