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 xxxconfig xxxoption 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接口中会执行以下操作:

  1. 统计section的个数,记录在变量CONFIG_NUM_SECTIONS中。
  2. 记录当前和所有section名称,分别保存在变量CONFIG_SECTIONCONFIG_SECTIONS中。
  3. 记录当前section类型,保存在变量CONFIG_${CONFIG_SECTION}_TYPE中。
  4. 然后判断是否设置了回调函数,如果设置则调用回调config_cb

option_cblist_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调用的,其中传入的参数为空,可以依据该条件判断是否到达结尾。