23 KiB
介绍
ini是常用的工程配置文件,语法简单。varch提供的ini解析器给定了简单可靠的api,可以很轻松的加载和生成ini文件。
简单介绍下ini规范,ini的组成部分包含了以下的部分。
注释
ini支持注释,注释独占一行
注释以'#'或';'为标识
注释标识在行首,前面允许有空格,但不允许为其他字符
例子:
###########################
# 这是注释
###########################
aaa ; 这后面的不算注释
节(section)
每一个section独占一行
section名称用"[]"方括号括起来,以最外围方括号包含的内容作为section的名称,其中方括号内也可以包含方括号在内的任意字符
section名对大小写敏感
section不能重复
例子:
[section]
键(key)
键与值组成键值对
键值对以':'号或者'='号分隔,分割符两端的允许有空格不参与解析(但建议不留空格)
键对大小写不敏感
键在同一个section内不允许重复,但是可以在不同的section内重复
值(value)
在键值分隔符后面的内容即为值
值允许有任意字符(包含'='和':'分割符等字符纳入值内)
值可以跨行,前提是换行的缩进得大于值的缩进,才纳入换行值得解析
例子:
# 下面这个例子是允许的
value1 = =aaaa;#[]
# 值允许分行,此[]括起来的将不被解析为section
value2 = 1
2
3
4
[sss]
windows系统下的system.ini文件
; for 16-bit app support
[386Enh]
woafont=dosapp.fon
EGA80WOA.FON=EGA80WOA.FON
EGA40WOA.FON=EGA40WOA.FON
CGA80WOA.FON=CGA80WOA.FON
CGA40WOA.FON=CGA40WOA.FON
[drivers]
wave=mmdrv.dll
timer=timer.drv
[mci]
接口
创建和删除ini对象
ini_t ini_create(void);
void ini_delete(ini_t ini);
其中ini_t为ini的结构体,创建方法则会返回一个空的ini对象,删除方法则是删除传入的ini对象。
添加和移除section
int ini_add_section(ini_t ini, const char* section);
int ini_remove_section(ini_t ini, const char* section);
首先是添加section方法,会在ini对象后面添加指定名称的section,添加成功返回1,失败返回0,比如名字重复就会导致添加失败。
然后是移除section方法,同样也是传入指定的section名,移除成功返回1,失败返回0,移除section会将该section下的所有键值对也会被移除。
获取section
int ini_section_index(ini_t ini, const char* section);
const char* ini_section_name(ini_t ini, int index);
这两个获取section的方法刚好是对称的方法,第一个是通过section名获取section所在索引(索引就是从ini文件从头到尾的section排序,索引从0开始),当找不到指定section则返回负数-1,而第二个则相反是通过索引来获取section名,当不存在这个索引的section时候则返回NULL。
varch的ini解析器底层对于section的存储采取了内置迭代器的单向链表,在每次调用的时候都会记录下当前的位置,然后在下一次调用时候就会先判断是否和前一次的位置一样,所以对于与前一次相同的调用时间复杂度为O(1),以及采用ini_section_name方法遍历时候,时间复杂度为O(n)。
设置和获取value
int ini_set_value(ini_t ini, const char* section, const char* key, const char* value);
const char* ini_get_value(ini_t ini, const char* section, const char* key);
设置value方法,就是将ini指定的section对应的key的value设为指定的value,这个方法除了修改已经存在的键值对之外,还用于添加键值对。首先是找有没有这个section,没有这个section就添加这个section,然后找这个key,找不到这个key就添加这个key,把value设置为指定value。成功返回1,失败返回0。
获取value则是查找有没有这个section下的key-value对,存在就返回value,不存在则返回NULL。
移除key-value
int ini_remove_key(ini_t ini, const char* section, const char* key);
此方法将移除指定section下指定key的key-value对,移除成功返回1,失败返回0。
获取key
int ini_key_index(ini_t ini, const char* section, const char* key);
const char* ini_key_name(ini_t ini, const char* section, int index);
获取key的方法和获取section方法很类似,分别通过key名找索引和通过索引找key名。找索引时候,失败就返回负数,找key名失败就返回NULL。
同样,存储键值对也是采用内置迭代器的单向链表,当key名或者索引和上次调用的一致的时候,时间复杂度为O(1),通过index正向遍历时间复杂度为O(n)。
获取计数
int ini_section_count(ini_t ini);
int ini_pair_count(ini_t ini, const char* section);
这个两个方法分别获取ini下section的计数和指定section下键值对的计数。
ini对象转储
char* ini_dumps(ini_t ini, int preset, int *len);
int ini_file_dump(ini_t ini, char* filename);
首先ini_dumps方法,将ini对象按格式转储为字符串。其中传入的preset参数,是预测转出来的字符串的长度,因为在将ini对象转为字符串的过程中,是一边转换一边重新分配空间的,如果预测的这个长度准的话,预先分配的空间足够,就可以提高重新分配空间的次数,提高转换效率。*len则是转换出来的字符串长度,传入NULL时候就是不获取长度。返回值则是转换出来的字符串,这个字符串是函数分配的,在结束使用需要free掉。
ini_file_dump方法则是在ini_dumps的基础上将ini转储到文件当中,filename传入文件名,返回值为转储的长度,负值表示转储失败。
ini对象加载
ini_t ini_loads(const char* text);
ini_t ini_file_load(const char* filename);
类似转储的方法,ini对象可以从字符串文本中加载,也可以从文件中加载。加载成功则会返回一个ini对象,失败则返回NULL。
ini加载错误
int ini_error_info(int* line, int* type);
varch的ini解析器提供了精准的错误识别,在ini加载的失败的时候可以调用此方法去定位错误位置和错误的类型。返回值为1表示当前解析出错了,0则为未出错。line输出错误所在行,type输出错误的类型。错误类型定义如下:
#define INI_E_OK (0) // ok
#define INI_E_BRACKETS (1) // missing brackets ']'
#define INI_E_DELIM (2) // missing delimiter
#define INI_E_KEY (3) // missing key
#define INI_E_SECTION (4) // missing section
#define INI_E_REKEY (5) // key repeat
#define INI_E_RESECTION (6) // section repeat
#define INI_E_MEMORY (7) // memory allocation failed
#define INI_E_OPEN (8) // fail to open file
参考例子
生成ini文件
static void test_dump(void)
{
ini_t ini = NULL; // 定义ini对象,习惯初始化为NULL
ini = ini_create(); // 创建空ini对象
if (ini == NULL)
{
printf("ini create fail!\r\n");
return;
}
/* 添加section */
ini_add_section(ini, "Zhang San");
ini_add_section(ini, "Li Si");
ini_add_section(ini, "Wang Wu");
/* 添加键值 */
ini_set_value(ini, "Zhang San", "age", "18");
ini_set_value(ini, "Zhang San", "height", "178");
ini_set_value(ini, "Zhang San", "email", "123456@qq.com");
ini_set_value(ini, "Li Si", "age", "20");
ini_set_value(ini, "Li Si", "gender", "man");
ini_set_value(ini, "Li Si", "weight", "65");
ini_set_value(ini, "Wang Wu", "age", "22");
/* 转储ini到文件 */
ini_file_dump(ini, WRITE_FILE);
ini_delete(ini); // 用完之后需要删除
}
转储的文件
[Zhang San]
age = 18
height = 178
email = 123456@qq.com
[Li Si]
age = 20
gender = man
weight = 65
[Wang Wu]
age = 22
例子里面使用函数很多没有对返回值进行判断,实际应用需对返回值进行判断。
加载ini文件
测试的文件如前面提到的windows系统下找到的一个ini文件 system.ini
; for 16-bit app support
[386Enh]
woafont=dosapp.fon
EGA80WOA.FON=EGA80WOA.FON
EGA40WOA.FON=EGA40WOA.FON
CGA80WOA.FON=CGA80WOA.FON
CGA40WOA.FON=CGA40WOA.FON
[drivers]
wave=mmdrv.dll
timer=timer.drv
[mci]
加在测试代码
void test(void)
{
ini_t ini = NULL; // 定义ini对象,习惯初始化为NULL
/* 加载ini文件 */
ini = ini_file_load("system.ini");
if (ini == NULL)
{
int line, type;
ini_error_info(&line, &type);
printf("ini parse error! line %d, error %d.\r\n", line, type);
return;
}
/* 遍历ini对象 */
int section_count = ini_section_count(ini); // 获取section计数
for (int i = 0; i < section_count; i++)
{
char *section_name = ini_section_name(ini, i); // 获取section名称
printf("section: [%s]\r\n", section_name);
int pair_count = ini_pair_count(ini, section_name); // 获取pair计数
for (int j = 0; j < pair_count; j++)
{
char *key = ini_key_name(ini, section_name, j); // 获取key名称
printf("key[%s], value[%s]\r\n", key, ini_get_value(ini, section_name, key)); // 打印键值对
}
}
ini_delete(ini); // 用完之后需要删除
}
运行结果:
section: [386Enh]
key[woafont], value[dosapp.fon]
key[EGA80WOA.FON], value[EGA80WOA.FON]
key[EGA40WOA.FON], value[EGA40WOA.FON]
key[CGA80WOA.FON], value[CGA80WOA.FON]
key[CGA40WOA.FON], value[CGA40WOA.FON]
section: [drivers]
key[wave], value[mmdrv.dll]
key[timer], value[timer.drv]
section: [mci]
加载错误
在上面例子的基础上把system.ini文件修改一下,把section [mci] 右边的括号"]"删掉,再加载。
; for 16-bit app support
[386Enh]
woafont=dosapp.fon
EGA80WOA.FON=EGA80WOA.FON
EGA40WOA.FON=EGA40WOA.FON
CGA80WOA.FON=CGA80WOA.FON
CGA40WOA.FON=CGA40WOA.FON
[drivers]
wave=mmdrv.dll
timer=timer.drv
[mci
运行结果:
ini parse error! line 13, error 1.
如此能定位到13行出现1号错误,也就是
#define INI_E_BRACKETS (1) // missing brackets ']'
源码解析
ini解析器结构体
ini解析器的所有结构体都是隐式的,也就是不能直接访问到结构体成员的,这样子的方式保证了模块的独立与安全,防止外部调用修改结构体的成员导致ini存储结构的破坏。所以ini解析器只留了唯一一个ini的声明在头文件,然后结构体的定义都在源文件。只能使用ini解析器提供的方法对ini对象进行操作。
ini类型声明
typedef struct INI* ini_t;
使用时候,只是用ini_t即可。
typedef struct INI
{
SECTION* sections; /* sections base */
ITERATOR iterator; /* section iterator */
int count; /* section count */
} INI;
INI结构体中包含了5个成员,sections(section的链表),count(section的计数),iterator(section的迭代器)。特别说明这个iterator,就是这个迭代器记录section访问时候的位置,当再一次访问检查到是同位置时候,就可以快速返回,而不用从头到尾再遍历。迭代器后续再说明。
typedef struct SECTION
{
struct SECTION *next; /* link */
char* name; /* section name */
PAIR* pairs; /* pairs base */
ITERATOR iterator; /* pair iterator */
int count; /* pair count */
} SECTION;
看SECTION结构体,包含了5个成员,next(指向下一个SECTION,形成单向链表),name(section名),pairs(键值对链表),iterator(pair的迭代器),count(pair的计数)。
typedef struct PAIR
{
struct PAIR *next; /* link */
char* key; /* key */
char* value; /* value */
} PAIR;
再看PAIR结构体,包含3个成员,next(指向下一个PAIR,形成单向链表),key(键),value(值)。
typedef struct
{
void *p; /* iteration pointer */
int i; /* iteration index */
} ITERATOR;
最后看看这个迭代器,其实这个迭代器很简单,就是记录当前所指向的单向链表的结点(成员p),以及记录当前所在单向链表的索引。具体是怎么记录的下文再聊。
结构体介绍到这里,INI类的存储结构已经很明了
INI
<it>
section[0] --> pair[0] --> pair[1] --> pair[2]
| k v k v k v
|
v <it>
section[1] --> pair[0] --> pair[1] --> pair[2]
| k v k v k v
|
v <it>
<it> section[2] --> pair[0] --> pair[1]
| k v k v
|
v <it>
section[3] --> pair[0]
. k v
.
.
单向链表的迭代
单向链表的操作不是这里的重点,这里内置的迭代器为了提高单向链表的访问效率,迭代过程着重说明下。以section迭代为说明,说明下这个迭代器获取的链表结点的过程:
static SECTION* ini_section(ini_t ini, int index) // 传入索引
{
if (index >= ini->count) return NULL; // 判断索引有没有越界
/*
这个一步是重置迭代,也就是将迭代器定位回到链表首位
满足其中3个条件之一都重置迭代器
1、因为单向链表,不能反向指向,所以目标索引小于迭代器的索引时候,就要重置,然后从链表首位迭代到指定索引
2、迭代器指针(p成员)为空时,为空则没有指向具体的结点,当然得重置迭代,所以外部想重置迭代器,只需将p成员置NULL即可
3、目标索引index为0时,主动获取第0位,也就是首位
*/
if (index < ini->iterator.i || !ini->iterator.p || index == 0)
{
ini->iterator.i = 0;
ini->iterator.p = ini->sections;
}
/*
循环将迭代器迭代到指定的索引位置
单向链表索引正向递增,所以正向遍历时候时间复杂度O(n),反向遍历还是O(n^2)
*/
while (ini->iterator.p && ini->iterator.i < index)
{
ini->iterator.p = ((SECTION *)(ini->iterator.p))->next;
ini->iterator.i++;
}
/* 返回迭代器指向的结点 */
return ini->iterator.p;
}
迭代器在对链表进行调整的时候需要相应调整,最简单的方式就是把成员p设为NULL重置迭代器。
这个迭代器在通过索引访问时候提高了效率,通过section名的访问呢?通过section名随机访问,则是首先判断当前迭代器指向section名称时候匹配,匹配上则直接返回,不匹配还是得从头到尾遍历来匹配。
ini转储说明
转储就是按格式将ini“打印”出来,不过的是,不是打印在控制台,而是打印在指定内存空间。那这个空间哪来的呢?是动态内存分配来的,分配多少呢?这就回到前文说到preset形参了,如果preset设置的够好,就可以一次性分配到合适的空间,不然得在转储的时候动态的调整空间去存放这些“打印”的字符了。
现在先来看维护这个打印空间的结构体:
typedef struct
{
char* address; /**< buffer base address */
unsigned int size; /**< size of buffer */
unsigned int end; /**< end of buffer used */
} BUFFER;
一共也就是3个成员,address(空间的基地址),size(空间的大小),end(已使用的结尾,索引)。
动态调整空间的过程:
static int expansion(BUFFER* buf, unsigned int needed) // neede为所需的追加的容量
{
char* address;
int size;
if (!buf || !buf->address) return 0;
/* 计算当前已使用的加上所需的一共所需多大的空间 */
needed += buf->end;
/* 计算当前的空间还能满足需要 */
if (needed <= buf->size) return 1; /* there is still enough space in the current buf */
/*
当前空间大小满足不了所需,重新计算能满足所需的空间
新的空间不是满足当前所需就行了的,还得留有余量,让下次追加时候用
不然每次追加一点容量都要重新分配空间,很浪费效率
而新分配的空间要多大才好,2的平方,也就是比所需大的最小的2的次方
为什么选择2的次方?
1、算法好实现,计算比指定值大的最小2的次方数好实现
2、空间利用率好,为 (1 + 2)/ 2 = 75%
3、空间重分配的次数小,比如最终需要128时候,每次定量10的空间新增需要13次,而2的次方只需7次。
*/
size = pow2gt(needed);
address = (char*)realloc(buf->address, size);
if (!address) return 0;
buf->size = size;
buf->address = address;
return 1;
}
以其中一段转储的代码为例,看看这个是怎么使用的
olen = strlen(sect->name); // 先计算section名多长
if (expansion(&p, olen + 3) == 0) goto FAIL; // 追加相应的空间,+3是放其他字符
/* 将字符复制到打印空间 */
p.address[p.end++] = '[';
for (k = 0; k < olen; k++)
{
p.address[p.end++] = sect->name[k];
}
p.address[p.end++] = ']';
p.address[p.end++] = '\n';
在转储value的时候有个点需要注意的,就是value是允许换行的,只是换行的缩进要大于其key的缩进,由于打印key时候是没有添加缩进的,所以value有换行的时候,就需要在换行后面插入一个缩进表明后面那行是归属于前面的键值对的。
/* get length of value */
olen = 0;
k = 0;
while (1)
{
if (!pair->value[olen]) break;
if (pair->value[olen] == '\n') k++; // 记录value换行数,以便插入缩进
olen++; // 记录value实际长度
}
if (expansion(&p, olen + k + 1) == 0) goto FAIL;
/* dump value */
for (k = 0; k < olen; k++)
{
p.address[p.end++] = pair->value[k];
if (pair->value[k] == '\n') p.address[p.end++] = '\t'; // 插入缩进
}
p.address[p.end++] = '\n';
最终打印时候就是将ini遍历,动态调整空间把每个section和键值对按顺序打印到指定空间。
ini加载说明
ini解析器最重要以及最复杂的部分就这个加载解析的部分。
-
1、首先创建一个ini空对象,然后在随着解析把解析到的section、key、value这些逐个添加到ini对象,等到解析完就可以得到一个完整的ini对象
-
2、在解析之前的首先得将解析的行号以及错误重置,然后随着解析遇到换行符就让行号相应的递增,碰到解析出错了,就记录下错误的类型
-
3、然后就到了真正的解析过程,解析的过程就是逐个字符的遍历判断,判断相应的字符段是什么范围的,是section?是key?是value?是注释?得到相应的范围就执行相应的解析动作。
整个解析就是在一个大循环当中
while (*text)
{
...
...
...
}
因为ini语法规定的内容基本都是以行来划分(value特殊情况可以换行),所以基本在这个循环里面就是在处理一行行的信息。
在while循环的第一步
s = skip(text);
depth = s - text;
text = s;
跳过无用的字符,这里无用的字符就比如空格或者tab这些没有实际意义的字符。在跳过字符的同时,记录下来缩进,也就是后面判断下一行是归属于新的一行还是上一行的value的范围的依据。
紧接着,就判断是不是注释了,因为注释是独占一行的,碰到注释标识了,后面的内容就不管了,跳过注释。
/* skip comments */
if (iscomment(*text))
{
text = lend(text); // 直接定位到行末
if (*text == '\n')
{
text++;
eline++;
continue;
}
}
剩下的就是规定section、key、value范围以及相应解析了。大体结构如下
/*
这个scope表示的是上一个value的界值范围
这个text解析的文本大于scope,也是超过上个value范围了,不属于上个value了
就可以解析新的section和key了
*/
if (text >= scope)
{
/* 以 [ 开头的表示section的范围 */
if (*text == '[')
{
/*
section是独占一行的,以最外围的 [] 括起来的section名
所以在这里就找最外围的 [] ,左边已经找到了,就找右边的
找最右的,首先就直接定位到行末,再从行末往回找
跳过无用的字符,找到右面 ]
*/
...
...
...
}
else
{
/*
key是必须在行首,不是section部分那就是key部分了
key范围的确定也就是找到第一个分割符 = 或者 : 了
找到了分割符,分割符前面的就是key的范围,后面就是value的范围
因为value的范围是可以换行的,所以这里要另外算value的范围
value换行的前提是,新行的缩进需要大于key的缩进(这里新行是空白行另算)
然后找个哨兵往前面探,探到不属于value的范围位置
*/
...
...
...
}
}
else
{
/*
还是属于上个value范围的时候
规定接下来每一行哪些字符块是有用的,也就是在这一行掐头去尾,把无用字符去掉
掐头去尾完之后,就把真正有意义的字符串追加到value后面
*/
...
...
...
}
这里特别说明下获取value的范围
static const char* value_scope(const char* text, int depth)
{
const char *s = text;
const char *sentinel = NULL;
s = lend(s); // 范围直接包含分割符 : = 后面的这行
while (*s)
{
sentinel = skip(s + 1); // 让哨兵走到新行的有意义字符
/* skip comments */
if (iscomment(*sentinel))
{
s = lend(sentinel);
continue;
}
/* 当前行的缩进大于key的缩进,这一行也纳入范围 */
if (sentinel - s - 1 > depth)
{
s = sentinel;
s = lend(s);
}
/* 缩进变小了,但不一定不属于范围 */
else
{
/* 碰到了有意义字符了,就不属于value范围了 */
if (*sentinel != 0 && *sentinel != '\n') break;
/* 那就直接到行末了,这行是空白行,还是属于value范围 */
else s = sentinel;
}
}
/* 倒退回去,去掉那些尾部是空白行的,不把这些空白行归属value范围 */
while (*s == 0 || *s == '\n')
{
sentinel = rskip(s - 1, text);
if (*sentinel != 0 && *sentinel != '\n') break;
s = sentinel;
}
return s;
}
ini的增删改查说明
剩下的针对ini增删改查的其实已经没什么特别说明的了,就是对链表数据结构的基本操作,这里不着重说明了