OpenWrt之uci.sh中uci_load的工作原理
OpenWrt在/lib/functions.sh
和/lib/config/uci.sh
提供了一组标准的shell接口来操作UCI配置,这样可以在Shell脚本中处理UCI配置。尤其是在/etc/init.d
目录下的配置文件中。
本文主要探索/lib/config/uci.sh
脚本中的uci_load
接口是如何将UCI配置加载到内存中的。
用法
下面以/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'
下面遍历/etc/config/network
文件中的interface
#!/bin/sh
. /lib/functions.sh
# 加载网络配置文件
uci_load network
# 定义处理每个接口的回调函数
handle_interface() {
local section="$1"
echo "section: $section"
config_get iface "$section" ifname
config_get device "$section" device
echo "Interface: $iface"
echo "Device: $device"
}
# 遍历所有接口
config_foreach handle_interface interface
输出结果如下:
root@OpenWrt:~/uci# ./uci_load_test.sh
section: loopback
Interface: lo
Device: lo
section: lan
Interface: br-lan
Device: br-lan
原理
在上面的代码中调用uci_load
之前首先引入了/lib/functions.sh
文件,这是因为该文件提供了一些uci_load
所需的参数。
uci_load
接口的源码如下:
CONFIG_APPEND=
uci_load() {
local PACKAGE="$1"
local DATA
local RET
local VAR
# 初始化,取消导出相关变量
# export -n用于取消导出变量
_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
# 从uci配置中获取相关配置,比如获取/etc/config/network配置
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"
}
NO_EXPORT=1
参数定义在/lib/functions.sh
脚本中,而${NO_EXPORT:+-n}
的含义是如果NO_EXPORT
有值则取-n
,因此export ${NO_EXPORT:+-n}
等价于export -n
,具体见export。
LOAD_STATE=1
参数定义在/lib/functions.sh
脚本中,UCI_CONFIG_DIR
参数未定义。
因此DATA="$(/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} ${LOAD_STATE:+-P /var/state} -S -n export "$PACKAGE" 2>/dev/null)"
等价于DATA="$(/sbin/uci -P /var/state -S -n export "$PACKAGE" 2>/dev/null)"
,uci
的参数见uci参数。
如果传入参数为network
,则为DATA="$(/sbin/uci -P /var/state -S -n export "network" 2>/dev/null)"
,那么输出结果为:
root@OpenWrt:~/uci# /sbin/uci -P /var/state -S -n export "network" 2>/dev/null
package network
config interface 'loopback'
option device 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
option up '1'
option ifname 'lo'
config globals 'globals'
option ula_prefix 'fdef:6d48:4a40::/48'
config device 'cfg030f15'
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'
option up '1'
option ifname 'br-lan'
接下来的两行现实判断执行结果,命令返回值是否为0以及DATA
参数是否为空。
然后重点就是eval "$DATA"
,该命令会将$DATA
的每一行内容作为命令来执行,具体见eval。
那么接下来的package xxx
、config xxx
、option xxx
以及list xxx
等行都会作为命令来执行,而这些命令都在/lib/functions.sh
脚本中提供。
/lib/functions.sh
脚本中的相关内容如下:
append() {
local var="$1"
local value="$2"
local sep="${3:- }"
eval "export ${NO_EXPORT:+-n} -- \"$var=\${$var:+\${$var}\${value:+\$sep}}\$value\""
}
package() {
return 0
}
config () {
local cfgtype="$1"
local name="$2"
export ${NO_EXPORT:+-n} CONFIG_NUM_SECTIONS=$((CONFIG_NUM_SECTIONS + 1))
name="${name:-cfg$CONFIG_NUM_SECTIONS}"
append CONFIG_SECTIONS "$name"
export ${NO_EXPORT:+-n} CONFIG_SECTION="$name"
config_set "$CONFIG_SECTION" "TYPE" "${cfgtype}"
[ -n "$NO_CALLBACK" ] || config_cb "$cfgtype" "$name"
}
option () {
local varname="$1"; shift
local value="$*"
config_set "$CONFIG_SECTION" "${varname}" "${value}"
[ -n "$NO_CALLBACK" ] || option_cb "$varname" "$*"
}
list() {
local varname="$1"; shift
local value="$*"
local len
config_get len "$CONFIG_SECTION" "${varname}_LENGTH" 0
[ $len = 0 ] && append CONFIG_LIST_STATE "${CONFIG_SECTION}_${varname}"
len=$((len + 1))
config_set "$CONFIG_SECTION" "${varname}_ITEM$len" "$value"
config_set "$CONFIG_SECTION" "${varname}_LENGTH" "$len"
append "CONFIG_${CONFIG_SECTION}_${varname}" "$value" "$LIST_SEP"
[ -n "$NO_CALLBACK" ] || list_cb "$varname" "$*"
}
config_set() {
local section="$1"
local option="$2"
local value="$3"
export ${NO_EXPORT:+-n} "CONFIG_${section}_${option}=${value}"
}
以config interface 'loopback'
为例,config()
函数接收两个参数。
加上注释之后并运行结果如下:
config () {
local cfgtype="$1"
local name="$2"
export ${NO_EXPORT:+-n} CONFIG_NUM_SECTIONS=$((CONFIG_NUM_SECTIONS + 1))
echo "CONFIG_NUM_SECTIONS=$CONFIG_NUM_SECTIONS"
name="${name:-cfg$CONFIG_NUM_SECTIONS}"
echo "name=$name"
append CONFIG_SECTIONS "$name"
echo "CONFIG_SECTIONS=$CONFIG_SECTIONS"
export ${NO_EXPORT:+-n} CONFIG_SECTION="$name"
echo "CONFIG_SECTION=$CONFIG_SECTION"
config_set "$CONFIG_SECTION" "TYPE" "${cfgtype}"
eval echo \${CONFIG_${CONFIG_SECTION}_TYPE}
[ -n "$NO_CALLBACK" ] || config_cb "$cfgtype" "$name"
}
# CONFIG_NUM_SECTIONS=1
# name=loopback
# CONFIG_SECTIONS=loopback
# CONFIG_SECTION=loopback
# interface
# CONFIG_NUM_SECTIONS=2
# name=globals
# CONFIG_SECTIONS=loopback globals
# CONFIG_SECTION=globals
# globals
# CONFIG_NUM_SECTIONS=3
# name=cfg030f15
# CONFIG_SECTIONS=loopback globals cfg030f15
# CONFIG_SECTION=cfg030f15
# device
# CONFIG_NUM_SECTIONS=4
# name=lan
# CONFIG_SECTIONS=loopback globals cfg030f15 lan
# CONFIG_SECTION=lan
# interface
在uci_load
调用过程中每遇到一个section
就会调用1次config()
接口,在config
接口中会执行以下操作:
- 统计
section
的个数,记录在变量CONFIG_NUM_SECTIONS
中。 - 记录当前和所有
section
名称,分别保存在变量CONFIG_SECTION
和CONFIG_SECTIONS
中。 - 记录当前
section
类型,保存在变量CONFIG_${CONFIG_SECTION}_TYPE
中。 - 然后判断是否设置了回调函数,如果设置则调用回调
config_cb
。
option_cb
和list_cb
同理,list_cb
可能要复杂一点。
uci_load
函数接下来就是删除DATA
变量,然后如果CONFIG_SECTION
再调用一次config_cb
回调函数。
eval命令
在Shell脚本中,eval
命令用于对传递给它的字符串进行重新解析和执行。它可以将字符串中的变量、命令等进行解析并执行,通常用于需要动态构建和执行命令的场景。
示例如下:
#!/bin/sh
# 示例1:解析变量
cmd="echo Hello, World!"
eval $cmd
# 示例2:动态构建命令
varname="myvar"
eval $varname="Hello"
echo $myvar
# 示例3:多重解析
cmd1="echo"
cmd2="Hello, World!"
eval "$cmd1 $cmd2"
# Hello, World!
# Hello
# Hello, World!
uci参数
root@OpenWrt:~/uci# uci
Usage: uci [<options>] <command> [<arguments>]
Commands:
batch
export [<config>]
import [<config>]
changes [<config>]
commit [<config>]
add <config> <section-type>
add_list <config>.<section>.<option>=<string>
del_list <config>.<section>.<option>=<string>
show [<config>[.<section>[.<option>]]]
get <config>.<section>[.<option>]
set <config>.<section>[.<option>]=<value>
delete <config>[.<section>[[.<option>][=<id>]]]
rename <config>.<section>[.<option>]=<name>
revert <config>[.<section>[.<option>]]
reorder <config>.<section>=<position>
Options:
-c <path> set the search path for config files (default: /etc/config)
-d <str> set the delimiter for list values in uci show
-f <file> use <file> as input instead of stdin
-m when importing, merge data into an existing package
-n name unnamed sections on export (default)
-N don't name unnamed sections
-p <path> add a search path for config change files
-P <path> add a search path for config change files and use as default
-t <path> set save path for config change files
-q quiet mode (don't print error messages)
-s force strict mode (stop on parser errors, default)
-S disable strict mode
-X do not use extended syntax on 'show'
-P <path>
参数用于增加一个配置的搜索路径。
-n
对导出的匿名section
进行命名。
export
export -n
用于取消导出变量,取消导出变量后,变量仍旧存在于当前Shell,但是不会存在于子进程中,验证脚本如下:
#!/bin/bash
# 设置并导出变量
export VAR1="Hello"
export VAR2="World"
# 显示当前变量值
echo "Before export -n:"
echo "VAR1: $VAR1"
echo "VAR2: $VAR2"
# 取消导出 VAR1
export -n VAR1
# 启动一个子 Shell 并检查变量
echo "In a subshell:"
bash -c 'echo "VAR1: $VAR1"; echo "VAR2: $VAR2"'
# 显示当前 Shell 中的变量值
echo "After export -n in current shell:"
echo "VAR1: $VAR1"
echo "VAR2: $VAR2"
# Before export -n:
# VAR1: Hello
# VAR2: World
# In a subshell:
# VAR1:
# VAR2: World
# After export -n in current shell:
# VAR1: Hello
# VAR2: World
示例
下面是将用到的/lib/config/uci.sh
和/lib/functions.sh
脚本中的所有接口汇总到一个脚本文件中执行的示例:
#!/bin/sh
_C=0
NO_EXPORT=1
LOAD_STATE=1
package() {
return 0
}
# config_get <variable> <section> <option> [<default>]
# config_get <section> <option>
config_get() {
case "$2${3:-$1}" in
*[!A-Za-z0-9_]*) : ;;
*)
case "$3" in
"") eval echo "\"\${CONFIG_${1}_${2}:-\${4}}\"";;
*) eval export ${NO_EXPORT:+-n} -- "${1}=\${CONFIG_${2}_${3}:-\${4}}";;
esac
;;
esac
}
append() {
local var="$1"
local value="$2"
local sep="${3:- }"
eval "export ${NO_EXPORT:+-n} -- \"$var=\${$var:+\${$var}\${value:+\$sep}}\$value\""
}
config () {
local cfgtype="$1"
local name="$2"
export ${NO_EXPORT:+-n} CONFIG_NUM_SECTIONS=$((CONFIG_NUM_SECTIONS + 1))
name="${name:-cfg$CONFIG_NUM_SECTIONS}"
append CONFIG_SECTIONS "$name"
export ${NO_EXPORT:+-n} CONFIG_SECTION="$name"
config_set "$CONFIG_SECTION" "TYPE" "${cfgtype}"
[ -n "$NO_CALLBACK" ] || config_cb "$cfgtype" "$name"
}
option () {
local varname="$1"; shift
local value="$*"
config_set "$CONFIG_SECTION" "${varname}" "${value}"
[ -n "$NO_CALLBACK" ] || option_cb "$varname" "$*"
}
list() {
local varname="$1"; shift
local value="$*"
local len
config_get len "$CONFIG_SECTION" "${varname}_LENGTH" 0
[ $len = 0 ] && append CONFIG_LIST_STATE "${CONFIG_SECTION}_${varname}"
len=$((len + 1))
config_set "$CONFIG_SECTION" "${varname}_ITEM$len" "$value"
config_set "$CONFIG_SECTION" "${varname}_LENGTH" "$len"
append "CONFIG_${CONFIG_SECTION}_${varname}" "$value" "$LIST_SEP"
[ -n "$NO_CALLBACK" ] || list_cb "$varname" "$*"
}
config_set() {
local section="$1"
local option="$2"
local value="$3"
export ${NO_EXPORT:+-n} "CONFIG_${section}_${option}=${value}"
}
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"
}
config_cb() {
local type="$1"
local name="$2"
# commands to be run for every section
echo "type=$type"
echo "name=$name"
}
option_cb() {
local name="$1"
local value="$2"
}
list_cb() {
local name="$1"
local value="$2"
}
uci_load "network"
执行结果如下:
type=interface
name=loopback
type=globals
name=globals
type=device
name=cfg030f15
type=interface
name=lan
type=
name=
最后一次执行config_cb
是由uci_load
调用的,其中传入的参数为空,可以依据该条件判断是否到达结尾。
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/openwrt/openwrt%E4%B9%8Buci.sh%E4%B8%ADuci_load%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。