现代C++工程实践:简单的IniParser 1

前言

这是笔者准备开的一个新坑,一个很自然的问题就是——笔者为什么要写这个系列的博客,他跟我们的其他C++工程教程有什么区别呢?

笔者注意到,大部分市场上的教程似乎都是语法律师,这不奇怪。毕竟,连如何编写一段可以运行的C++程序都做不到的人,是没办法继续使用C++解决实际问题的,就像你不可能指望一个连话都不会说的哑巴去做能说会道的销售一样。所以笔者建议你,如果你还没有学会如何编写Hello World,还是不要继续我们的旅程,也许其他大佬的C++教程会更加适合您。

但是笔者注意到,不少人在啃完厚厚的《C++ Primer》之后就不知道做什么了,市面上大家也只会呼呼塞给你各式各样的源码,然后一走了之,这对于新手而言的确有些残酷了(实际上,直到笔者学习C++一年后才有勇气去看看C++一些大型工程的源代码,这其中也算是克服了不少的心里困难)。笔者注意到,最大批的人只是在教授你如何编写符合C++语法的代码;剩下的,如何使用C++来解决工程实际问题,原谅笔者孤陋寡闻,实在没看到太多的人在做。所以笔者决定自己做一个尝试——即从工程实际反向思考我们应该如何选择更好的编写C++代码,而不是为了使用现代的C++特性炫技才刻意的解决问题(这是一种本末倒置,笔者一向认为技术是解决问题的,而不是装逼的)。笔者水平不佳,自己也是在一边编写一边尝试思考,给看官分析笔者的思路。如果有任何您不满意,或者是您认为存在错误的地方,欢迎来到Awesome-Embedded-Learning-Studio/Tutorial_cpp_SimpleIniParser吐槽!

从目的出发

不要着急动手,假设,我们编写代码的时候,的确遇到了一个问题——手头没有ini parser,需要我们自己写一个(现实生活中很不可能,大把的IniParser随便用,Python甚至内置了相关的处理的库,C++至少我知道Qt有QSettings)。

所以,我们的目的很明确了——编写一个简单的Ini Parser,使其基本可以完成基本.ini文件的解析。但是这个需求我相信聪明的您看到这个仓库的时候脑子就有这个概念了。所以下一步,我们需要观察被解决问题的对象特征是如何的。

解析ini文件,我们就需要Ini文件的基本规范。后续的改进,是伴随着我们发现了本应该完成的功能但是挂了bug,排查修复改进;亦或者是对被建模对象更进一步的认识,再进一步迭代(比如说后面你的用户扔了Request:嘿!你搞错了,这个在规范中是支持的!你返回了我不期待的结果)。所以,为了进一步前进。我们还需要了解一下Ini文件的基础规范是如何的。

什么是Ini文件?条条框框列出来!

如果您对ini文件足够熟悉,可以直接跳过本小节——没必要在这里浪费事件!

INI 文件(Initialization File) 是一种非常古老、轻量且常用的配置文件格式,最早来源于 Windows 系统(Windows 3.0 时代)。它的核心目标就是使用 简单可读性强 的文本格式存储分段式配置。

笔者PS:就是因为他足够简单我才选的,要是parse其他东西,我估计我是写不完(笑

Ini文件是的确有在用的——Windows 下大量软件使用 INI(尽管,现代软件也可能用 JSON、YAML)。对于一些比较简单的场景,比如说嵌入式设备,特别是 MCU、Linux Embedded 设备环境中,INI 因为简单可靠,非常适合保存少量 Key-Value 配置。

扩展一下,后续我们的朋友在做越来越大的系统的时候,肯定还要遇到指导软件系统初始化状态设置的问题,所以这里给一个小表格:

格式结构化能力可读性常见用途特点
INI软件配置简单、解析器实现极轻量
JSON中等中等Web、API严格、结构化好、注释不支持
YAMLDevOps、配置支持层级、可读性强,但语法复杂
TOMLRust 项目生态类 INI,但更现代、更严格、更结构化

Ini文件的格式大致上就是如下的语法,笔者先给出一段合法的ini file

; this is a comment
# also a comment
; top-level keys
topkey = topvalue

[server]
host = example.com
port= 8080
path = "/api/v1/resource" ; inline comment
escaped = "line\nnew"  # another inline comment

[database]
user = dbuser
password = "p@ss;word" ; note semicolon inside quoted value
timeout=30

空行允许,需忽略

空行可随处出现,不影响解析。


注释支持两种前缀 ;#

行首注释:
; comment
# comment
行尾注释(inline comment):
key = value ; inline comment
key = value # inline comment

但注意:如果注释符号在字符串引号内部,应视为普通字符,如:

password = "p@ss;word" ; <-- 此处 ; 是值的一部分

Section(节)定义

以方括号包裹:

[server]
[database]

Section 名称:

  • 通常不允许嵌套(例如 [a.b] 不表示树形)
  • 允许任意可打印字符(不包含 ]

自动归并到上一级

如你的示例:

topkey = topvalue

在没有 section 时,它属于 default section空 section

你的解析器应支持:

get("", "topkey") == topvalue

Key-Value 语法

基本形式:

key = value
key= value
key =value
key=value

等号两侧可有可无空格。


键(Key)的规范

  • 通常只允许 ASCII 字母数字,下划线
  • 但很多 parser接受任意可打印字符直到遇到 =
  • Key 前后应 trim

值(Value)的规范

值的来源分两类:


未加引号的值
host = example.com
port = 8080
timeout=30

特征:

  • = 后直到行尾(或注释符号前)为 value
  • 需要 trim
  • 支持空值(例如 key =

使用双引号包裹的值
path = "/api/v1/resource"
escaped = "line\nnew"
password = "p@ss;word"

特征:

  • "value..." 的内容为实际值
  • 引号内允许存在:
    • ;# 等字符
    • 转义字符,如 \n\"
"line\nnew"   →  line<newline>new
"p@ss;word"   →  p@ss;word

引号内部不截断注释,不提前结束 value。


Inline comment 规则

如下两种属于 inline comment:

value ; comment
value # comment

但前提是当前处于“引号外部”状态。


转义字符处理(示例中有体现)

如:

escaped = "line\nnew"

\n 应转换为换行字符。


多个等号的情况

a=b=c

常见处理方式:

  • 以第一个 = 左边为 key
  • 右边整个 b=c 作为 value

INI 文件解析器支持的语法规范

基础结构

  • 文件可包含空行、注释、节(Section)、键值对。
  • 字符编码默认 UTF-8。

注释

  • 行首注释以 ;# 开头。
  • 行内注释(inline comment)以 ;# 开始,但仅在未在引号内时生效。

Section

  • 形式:[section_name]
  • Section 之间互相独立。
  • 未进入任何 Section 时的 key 属于默认的上一级section,如果没有直接归属为""(空节)

Key-Value

  • 格式:key = value= 两侧允许空格)
  • Key 或 Value 前后空白应 trim。
  • Key 允许任意非空白字符(直到 = 为止)。
  • 一个 key 对应一个字符串值。

Value 两种模式

1. 未引号值

  • = 后至行尾(或注释符号前)。
  • 自动 trim。

2. 双引号值 "value"

  • 内部字符不受 inline comment 限制。
  • 允许 ;#、其它字符。
  • 支持转义字符:\n, \", \\ 等。

特殊 value 情况

  • key= 允许空值。
  • a=b=c 解析为 key a,value b=c
  • 值尾部注释在引号外时有效:value ; comment

继续

头大了?没事不要着急。我们一步一步来,工程就是这样,丢给一个我们大概要完成的事情,具体的拆解分析我们的需求步骤,严格评估我们到底要实现什么样的需求,然后一步一步拆解。下一篇,我们开始讨论如何分解这个需求,进一步的完成一些简单的case。