Rust 学习 实例 01
archive time: 2022-06-19
这是一个实战的例子, 可以帮助掌握之前的内容
介绍
这个实例里面, 我们要写一个类似 grep
功能的小程序, 即从文本或文件中找到对应文本的行
不过我们这个例子就不支持 regex 和一些高级功能了, 仅仅是查找对应内容所在的行的内容
代码
实例的代码如下
/// lib.rs
#![feature(io_error_more)]
use std::env;
use std::fs;
use std::io::ErrorKind;
use std::path;
#[derive(Debug)]
pub struct Config {
query: String,
filename: String,
conf: Vec<String>,
}
impl Config {
pub fn new() -> Result<Self, String> {
let args = env::args().collect::<Vec<_>>();
if args.len() < 3 {
Err(format!("no enough arguments\n{}", print_help()))
} else {
let conf = args
.iter()
.filter(|&s| s.starts_with("-"))
.map(|s| s.clone())
.collect::<Vec<_>>();
let args = args
.iter()
.filter(|&s| !(conf.contains(s) || s == &args[0]))
.take(2)
.collect::<Vec<_>>();
Ok(Config {
query: args[0].clone(),
filename: args[1].clone(),
conf,
})
}
}
pub fn get_file(&self) -> &String {
&self.filename
}
pub fn get_query(&self) -> &String {
&self.query
}
pub fn get_conf(&self) -> &Vec<String> {
&self.conf
}
}
pub fn print_help() -> String {
format!(
"\n{}\n{}\n{}\n{}\n{}\n",
"Usage: learn_rs <text> <file> [<args>]",
"available args:",
" `-h` or `--help`: print this message",
" `-c` or `--case`: case sensitive (default)",
" `-i` or `--insensitive`: case insensitive"
)
}
pub fn run(config: Config) -> Result<(), ErrorKind> {
let available_configs = vec!["-h", "--help", "-c", "--case", "-i", "--insensitive"];
if config
.get_conf()
.iter()
.filter(|&s| !available_configs.contains(&s.as_str()))
.count()
> 0
{
// check invalid config
Err(ErrorKind::InvalidInput)
} else if config.get_conf().contains(&"-h".to_string())
|| config.get_conf().contains(&"-help".to_string())
{
// get helps
Ok(println!("{}", print_help()))
} else if path::Path::new(config.get_file()).exists() {
// find pattern
let contents = fs::read_to_string(config.get_file()).unwrap();
if config.get_conf().contains(&"-i".to_string())
|| config.get_conf().contains(&"--insensitive".to_string())
{
Ok(search_insensitive(config.get_query(), &contents)
.iter()
.for_each(|&s| println!("{}", s)))
} else {
Ok(search(config.get_query(), &contents)
.iter()
.for_each(|&s| println!("{}", s)))
}
} else {
Err(ErrorKind::InvalidFilename)
}
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|&s| s.contains(query))
.collect::<Vec<_>>()
}
pub fn search_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|&s| s.to_lowercase().contains(&query.to_lowercase()))
.collect::<Vec<_>>()
}
/// main.rs
use learn_rust::{print_help, run, Config};
use std::process;
fn main() {
let conf = Config::new().unwrap_or_else(|err| {
eprintln!("\n>> Initializion Failed: `{}`\n", err);
process::exit(1);
});
if let Err(e) = run(conf) {
eprintln!("\n>> Runtime Error: `{}`\n", e);
println!("{}", print_help());
process::exit(1);
}
}
解释
我们从头开始看代码
使用 feature
首先是 <projRoot>/src/lib.rs
里的内容, 第一行
#![feature(io_error_more)]
这里是启用 unstable feature, 因为我使用了 ErrorKind::InvalidFilename
这样一个枚举,
而这个枚举项被标记为 unstable
, 所以需要启用这个 feature 才能启用
导入需要的库
然后是导入需要的库, 都是标准库下就存在的
use std::env;
use std::fs;
use std::io::ErrorKind;
use std::path;
分别是使用参数, 读取文件, 以及一些错误的定义 和 检查文件是否存在
定义类型
我们从命令行收到的参数可以用一个类型来包裹, 我们称其为 Config
#[derive(Debug)]
pub struct Config {
query: String,
filename: String,
conf: Vec<String>,
}
其中 query
表示要查询的内容, filename
表示要从哪个文件里查找, conf
则是一些选项
实现方法
对于我们自定义的类型, 我们还需要实现一些方法来供我们使用
impl Config {
pub fn new() -> Result<Self, String> {
let args = env::args().collect::<Vec<_>>();
if args.len() < 3 {
Err(format!("no enough arguments\n{}", print_help()))
} else {
let conf = args
.iter()
.filter(|&s| s.starts_with("-"))
.map(|s| s.clone())
.collect::<Vec<_>>();
let args = args
.iter()
.filter(|&s| !(conf.contains(s) || s == &args[0]))
.take(2)
.collect::<Vec<_>>();
Ok(Config {
query: args[0].clone(),
filename: args[1].clone(),
conf,
})
}
}
pub fn get_file(&self) -> &String {
&self.filename
}
pub fn get_query(&self) -> &String {
&self.query
}
pub fn get_conf(&self) -> &Vec<String> {
&self.conf
}
}
首先就是 new()
方法, 这是个惯例, 实例化一个类型对象的方法一般被称为 new()
, 不过这不是必须的
我们的 new()
方法返回的是一个 Result<T, E>
类型, 这是因为有可能用户在使用时没有正确的传入对应参数
所以我们需要进行一些处理, 如果没有正确传入参数就返回一个错误 Err(String)
如果有足够的参数, 那么我们要对参数进行简单分类
如果参数前面有 -
, 那么这个参数就是个 conf
let conf = args
.iter()
.filter(|&s| s.starts_with("-"))
.map(|s| s.clone())
.collect::<Vec<_>>();
这里我们使用了 iter()
方法将参数转变为迭代器
然后使用 filter()
方法筛选出符合条件的参数, 使用 collect()
方法将筛选出来的的参数封装成一个 Vec<T>
类型
除了这些选项, 剩余的就是给程序的参数
let args = args
.iter()
.filter(|&s| !(conf.contains(s) || s == &args[0]))
.take(2)
.collect::<Vec<_>>();
由于我们只需要两个参数, 即 需要查询的内容 和 文件地址, 我们使用 take()
方法来截断
最后将筛选出来的内容包装成 Config
类型
Config {
query: args[0].clone(),
filename: args[1].clone(),
conf,
}
由于我们不允许直接修改 Config
类型的字段, 所以我们需要设置 getter
并将字段隐藏,
即不设置字段为 pub
, 而是使用 get_xxx()
这种方法来或许字段内容
实现函数
对于程序的功能我们需要使用函数来实现
比如打印帮助信息
pub fn print_help() -> String {
format!(
"\n{}\n{}\n{}\n{}\n{}\n",
"Usage: learn_rs <text> <file> [<args>]",
"available args:",
" `-h` or `--help`: print this message",
" `-c` or `--case`: case sensitive (default)",
" `-i` or `--insensitive`: case insensitive"
)
}
然后就是搜索, 也就是程序的主逻辑
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|&s| s.contains(query))
.collect::<Vec<_>>()
}
pub fn search_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|&s| s.to_lowercase().contains(&query.to_lowercase()))
.collect::<Vec<_>>()
}
这里我们分成了两个函数, 也就是大小写敏感和大小写不敏感
我们首先将文件内容读取为字符串, 然后使用 lines()
函数将字符串按行分割成一个迭代器
然后使用 filter()
来筛选, 使用字符串中的 contains()
方法来查询我们要查询的内容是否在这一行
然后我们将符合条件的行封装到一个 Vec<T>
里返回
对于大小写不敏感, 我们只需要将查询内容和文本内容都 to_lowercase()
即可
封装逻辑
最后, 我们需要将这些函数的逻辑封装成一个单独的函数
pub fn run(config: Config) -> Result<(), ErrorKind> {
let available_configs = vec!["-h", "--help", "-c", "--case", "-i", "--insensitive"];
if config
.get_conf()
.iter()
.filter(|&s| !available_configs.contains(&s.as_str()))
.count()
> 0
{
// check invalid config
Err(ErrorKind::InvalidInput)
} else if config.get_conf().contains(&"-h".to_string())
|| config.get_conf().contains(&"-help".to_string())
{
// get helps
Ok(println!("{}", print_help()))
} else if path::Path::new(config.get_file()).exists() {
// find pattern
let contents = fs::read_to_string(config.get_file()).unwrap();
if config.get_conf().contains(&"-i".to_string())
|| config.get_conf().contains(&"--insensitive".to_string())
{
Ok(search_insensitive(config.get_query(), &contents)
.iter()
.for_each(|&s| println!("{}", s)))
} else {
Ok(search(config.get_query(), &contents)
.iter()
.for_each(|&s| println!("{}", s)))
}
} else {
Err(ErrorKind::InvalidFilename)
}
}
这里就不一一讲解了
调用
我们写好了函数后就可以在 <projRoot>/src/main.rs
里调用
use learn_rust::{print_help, run, Config};
use std::process;
fn main() {
let conf = Config::new().unwrap_or_else(|err| {
eprintln!("\n>> Initializion Failed: `{}`\n", err);
process::exit(1);
});
if let Err(e) = run(conf) {
eprintln!("\n>> Runtime Error: `{}`\n", e);
println!("{}", print_help());
process::exit(1);
}
}
这里, std::process
库是为在发生错误时退出程序
由于我们以及封装好了程序逻辑, 所以在调用时只需要调用 run(conf)
即可
以上就是一个 Rust 练手的小例子, 希望大家能有所收获