问题
在之前的程序中,文本分割符皆以硬编码的方式出现,导致程序灵活性较差。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 配置文件的资料请关注好代码网其它相关文章!