问题
在之前的程序中,文本分割符皆以硬编码的方式出现,导致程序灵活性较差。rzeo 项目解析的文本,我倾向为下例所示形式:
以下 Rust 程序
@ hello world #
fn main() {
println!("Hello world!");
}
@
可在终端打印「Hello world!」。但是对于其他 rzeo 的用户而言,未必喜欢使用 @ 和 # 之类的符号。为了最大程度兼容所有人的偏好,rzeo 使用的文本分割符需以配置文件的方式进行定义,在其运行时方知文本分割符的具体形式。
rzeo 的配置文件采用 YAML 语言撰写,例如:
border: '\n[ \t]*@[ \t\n]*' code_snippet_neck: '[ \t]*#[ \t]*\n'
假设 rzeo 配置文件为 rzeo.conf,编写一个程序从该文件获取分割符。
读取文件
首先,考虑如何使用 Rust 标准库提供的文件读写功能,读取配置文件 rzeo.conf,并输出其内容,以熟悉文件读写功能的基本用法。
以下代码可读取 rzeo.conf 文件并逐行打印其内容:
use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() -> Result<(), std::io::Error> {
let f = File::open("rzeo.conf")?;
let reader = BufReader::new(f);
for line in reader.lines() {
println!("{}", line?);
}
return Ok(());
}BufRead 是特性。BufReader 是实现了 BufRead 特性的结构体类型,用于将硬盘中的文件内容读入到内存缓冲区以降低硬盘读取次数。需要注意的是,File 的 open 方法和 BufReader 的 lines 方法皆返回 Result<T, std::io::Error> 类型,T 为 String 类型——Rust 的又一种字符串类型,相当于 Vec<char> 类型。在此不对 String 给予讲解,可在使用中逐渐熟悉其用法。
以下代码可将 rzeo.conf 文件中的内容写入另一个文件:
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
fn main() -> Result<(), std::io::Error> {
let f = File::open("rzeo.conf")?;
let mut g = File::create("foo.txt")?;
let reader = BufReader::new(f);
for line in reader.lines() {
let content = line?;
g.write_all(content.as_bytes())?;
g.write_all("\n".as_bytes())?;
}
return Ok(());
}注意,File 的 write_all 方法,其参数类型为 &[u8],即字节数组切片,故而需要使用 &str 或 String 类型的 as_bytes 将字符串转化为字节数组切片。write_all 的返回值是 Result<()> 类型,需使用 unwrap 解包或使用 ? 进行错误传播。
路径
为了保持不同操作系统中文件路径的兼容性,Rust 标准库提供了一种特殊的字符串类型 std::path::Path 以及一些用于处理文件路径的方法。为了程序的可移植性,建议使用 std::path::Path 代替普通的字符串作为文件路径。
以下代码演示了 std::path::Path 的基本用法:
use std::path::Path;
use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() -> Result<(), std::io::Error> {
let path = Path::new("/tmp/rzeo.conf");
println!("{:?}", path);
let f = File::open(path)?;
let reader = BufReader::new(f);
for line in reader.lines() {
println!("{}", line?);
}
return Ok(());
}引入 serde 库
读取 rzeo.conf 文件并不困难,困难的是对其内容的解析。当前的 rzeo.conf 文件中的内容,仅仅用到了 YAML 最为基础的语法——键值对,即便如此,要对其予以解析,免不了要写许多代码。Rust 第三方库 serde 能够实现 Rust 语言的值与特定格式的数据文件的交换,即值的序列化(Serialize)和反序列化(Deserialize)。
serde 只是一个框架,对于特定格式的数据文件,需要引入 serde 的相应实现。下面使用 cargo 构建一个项目,引入 serde 和 serde_yaml 库,实现 Rust 结构体的序列化。
首先,使用 cargo 建立新项目并进入项目目录:
$ cargo new foo $ cd foo
然后使用 cargo add 命令添加 serde(同时开启 serde 的 derive 特性)和 serde_yaml 库:
$ cargo add -F derive serde $ cargo add serde_yaml
上述命令可在项目根目录下的 Cargo.toml 文件的 [dependencies] 部分添加以下内容:
serde = { version = "1.0.171", features = ["derive"] }
serde_yaml = "0.9.22"随着 serde 和 serde_yaml 库的更新,等你看到这份文档时,也动手搭建这个项目时,库的版本号应该是与上述内容不同。
序列化与反序列化
编辑上一节构建的 foo 项目的 src/main.rs 文件,令其内容为
use std::fs::File;
#[derive(serde::Serialize)]
struct Foo<'a> {
id: u32,
data: &'a str
}
fn main() -> Result<(), std::io::Error> {
let foo = Foo { id: 1, data: "Hello world!" };
let f = File::create("foo.yml")?;
serde_yaml::to_writer(f, &foo)?;
return Ok(());
}执行以下命令,编译并运行程序:
$ cargo run
但是上述的 main.rs 中存在错误,导致 Rust 编译器报错。错误的原因是 File::create 和 sert_yaml::to_writer 返回的 Result<T, E> 类型不一致,导致无法给出 main 函数的返回值类型的正确定义。对于上述代码快速而脏的修复是
serde_yaml::to_writer(f, &foo).unwrap();
即放弃 serde_yaml::to_write 的错误进行传播。
上述程序通过编译,运行结果是在当前目录创建 foo.yml 文件,其内容为
id: 1
data: Hello world!
以下代码实现了 YAML 文件 foo.yml 的反序列化:
use std::fs::File;
#[derive(Debug, serde::Deserialize)]
struct Foo {
id: u32,
data: String
}
fn main() -> Result<(), std::io::Error> {
let f = File::open("foo.yml")?;
let foo: Foo = serde_yaml::from_reader(f).unwrap();
println!("{:?}", foo);
return Ok(());
}foo.yml 中的内容被转换为 Rust 结构体类型 Foo 的实例 foo。需要注意的是,上述 Foo 的 data 域,其类型不再是 &str,而是 String,原因 serde_yaml::from_reader 方法并不占有数据,导致存储反序列化结果的结构体实例中的引用无效。
分割符
现在,定义一个结构体类型 Separator,用于存储从 rzeo.conf 中获取的分割符:
#[derive(Debug, serde::Deserialize)]
struct Separator {
border: String,
code_snippet_neck: String
}使用以下代码便可解析 rzeo.conf 相应的信息并将解析结果作为 Separator 类型的值:
fn main() -> Result<(), std::io::Error> {
let f = std::fs::File::open("rzeo.conf")?;
let foo: Separator = serde_yaml::from_reader(f).unwrap();
println!("{:?}", foo);
return Ok(());
}至此,本章开头提出的问题便得以解决。
C 版本
有一些采用 C 语言编写的库也能够实现对 YAML 文件的解析,例如能够支持 C 语言结构体的 YAML 序列化和反序列化的库 libcyaml,在 Ubuntu 系统可使用以下命令安装该库:
$ sudo apt install libyaml-dev libcyaml-dev
以下是反序列化 rzeo.conf 文件的 C 程序:
#include <stdlib.h>
#include <stdio.h>
#include <cyaml/cyaml.h>
typedef struct {
char *border;
char *code_snippet_neck;
} Foo;
/* 构造结构体类型 Foo 与 rzeo.conf 之间的联系 */
static const cyaml_schema_field_t top_mapping[] = {
CYAML_FIELD_STRING_PTR(
"border", CYAML_FLAG_POINTER, Foo, border, 0, CYAML_UNLIMITED),
CYAML_FIELD_STRING_PTR(
"code_snippet_neck", CYAML_FLAG_POINTER, Foo,
code_snippet_neck, 0, CYAML_UNLIMITED),
CYAML_FIELD_END
};
static const cyaml_schema_value_t top = {
CYAML_VALUE_MAPPING(CYAML_FLAG_POINTER, Foo, top_mapping)
};
/* 解析器设置 */
static const cyaml_config_t config = {
.log_fn = cyaml_log,
.mem_fn = cyaml_mem,
.log_level = CYAML_LOG_WARNING
};
/* 反序列化 */
int main(void) {
Foo *foo;
cyaml_err_t err = cyaml_load_file("rzeo.conf", &config, &top,
(cyaml_data_t **)&foo, NULL);
if (err != CYAML_OK) {
fprintf(stderr, "ERROR: %s\n", cyaml_strerror(err));
return EXIT_FAILURE;
}
printf("border: %s\n", foo->border);
printf("code_snippet_neck: %s\n", foo->code_snippet_neck);
cyaml_free(&config, &top, foo, 0);
return 0;
}使用以下命令编译上述程序:
$ gcc -o foo foo.c $(pkg-config --cflags --libs libcyaml)
运行程序:
$ ./foo border: \n[ \t]*@[ \t\n]* code_snippet_neck: [ \t]*#[ \t]*\n
小结
Rust 第三方库对 YAML 序列化和反序列化的支持优于 C 的第三方库。必须要承认,C 语言在文本处理方面,若想变得更为优雅,最好的办法是先基于它实现一门小巧的动态语言(例如 Lua 语言),由后者负责处理文本。
以上就是Rust 配置文件内容及使用全面讲解的详细内容,更多关于Rust 配置文件的资料请关注好代码网其它相关文章!




