rust 的運行速度、安全性、單二進位文件輸出和跨平臺支持使其成為構建命令行程式的最佳選擇。 實現一個命令行搜索工具`grep`,可以在指定文件中搜索指定的字元串。想實現這個功能呢,可以按照以下邏輯流程處理: 1. 獲取輸入文件路徑、需要搜索的字元串 2. 讀取文件; 3. 在文件內容中查找字元串所 ...
rust 的運行速度、安全性、單二進位文件輸出和跨平臺支持使其成為構建命令行程式的最佳選擇。
實現一個命令行搜索工具grep
,可以在指定文件中搜索指定的字元串。想實現這個功能呢,可以按照以下邏輯流程處理:
- 獲取輸入文件路徑、需要搜索的字元串
- 讀取文件;
- 在文件內容中查找字元串所在的行
- 列印包含字元串所在的行信息
創建項目ifun-grep
$> cargo new ifun-grep
項目在運行時,可以獲取到傳遞的參數。比如cargo run -- hboot hello.txt
,在文件hello.txt
查找字元串hboot
讀取參數
首先要先獲取到傳入的參數。通過標準庫std::env::args
獲取
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
collect()
方法可以將傳入的參數轉換為一個集合。對於變數args
必須註明集合類型。
參數的第一個值是二進位文件的名稱。可以用於程式調試或者列印出文件路徑,取出另外兩個參數,保存進對應的變數。方便後續傳參數使用。
let search = &args[1];
let file_path = &args[2];
println!("will search {} in {}", search, file_path)
讀取文件
首先創建測試文件hello.txt
,並寫入一段文字。
獨立寒秋,湘江北去,橘子洲頭。
看萬山紅遍,層林盡染;漫江碧透,百舸爭流。
鷹擊長空,魚翔淺底,萬類霜天競自由。
悵寥廓,問蒼茫大地,誰主沉浮?
攜來百侶曾游,憶往昔崢嶸歲月稠。
恰同學少年,風華正茂;書生意氣,揮斥方遒。
指點江山,激揚文字,糞土當年萬戶侯。
曾記否,到中流擊水,浪遏飛舟
讀取文件,並列印出文件中的內容。
let content = fs::read_to_string(file_path).expect("you should permission to read the file");
println!("read the content:\n{content}")
通過fs
模塊的read_to_string
方法讀取文件內容。expect
則用於處理讀取文件時發生的錯誤的提示信息,這在下麵的錯誤處理會有說明。
模塊拆分與錯誤處理
現在所有的處理業務都放在src/main.rs
中。取參和讀取文件是兩個不同功能的邏輯處理,當功能越來越複雜的時候,就應該關註分離。這在我們設計時可提前考慮好
main.rs
只被用來處理程式的執行。其他需要處理的邏輯則可以放在srr/lib.rs
中。
定義一個解析取參的函數parse_args
,現在仍然定義在src/main.rs
中。
fn parse_args(args: &Vec<String>) -> (&str, &str) {
let search = &args[1];
let file_path = &args[2];
(search, file_path)
}
fn main(){
let args: Vec<String> = env::args().collect();
let (search, file_path) = parse_args(&args);
println!("will search {} in {}", search, file_path);
}
這樣main
函數不再處理哪個參數對應哪個變數。
我們可以將這一組相關的變數通過結構體定義相互關聯起來。這樣函數返回將不再使用元組,並且可以通過結構體實例可以訪問到每一個屬性。
struct Config {
search: String,
file_path: String,
}
fn parse_args(args: &Vec<String>) -> Config {
let search = args[1].clone();
let file_path = args[2].clone();
Config { search, file_path }
}
fn main(){
let args: Vec<String> = env::args().collect();
let config = parse_args(&args);
println!("will search {} in {}", search, file_path);
}
在結構體中,實例化賦值需要擁有這些變數值的所有權。而變數args
是所有權的擁有者,通過clone()
方法拷貝一份數據。
可以看到parse_args
返回來一個結構體 Config 的實例,可以通過定義結構體的內部方法來創建實例。
impl Config {
fn new(args: &Vec<String>) -> Self {
let search = args[1].clone();
let file_path = args[2].clone();
Config { search, file_path }
}
}
fn main(){
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("will search {} in {}", search, file_path);
}
這樣,就不需要parse_args
函數了,通過結構體的內部方法實例化實例。
錯誤處理
如果我們執行cargo run
時,不傳遞任何參數,則程式會報錯。這樣的提示對於用戶並不友好。
首先可以通過判斷參數需要的參數信息,說明錯誤信息。
impl Config {
fn new(args: &Vec<String>) -> Self {
if args.len() < 3 {
panic!("至少傳入2個參數")
}
// ...
}
}
提示用戶必須傳入 2 個從參數,因為有一個預設的路徑參數。所以判斷不能少於3
除了直接提示錯誤信息並中斷程式,也可以使用Result
傳遞錯誤,讓主函數做決定如何去處理。
impl Config {
fn build(args: &Vec<String>) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("至少傳入2個參數");
}
let search = args[1].clone();
let file_path = args[2].clone();
Ok(Config { search, file_path })
}
}
現在提供了一個方法build
來處理這個邏輯,之前的new
不用了(這裡是語義話定義,new
常常表示不會產生錯誤),當有錯誤時,不是直接終止程式,而是返回一個Err
值。
在src/main.rs
中調用並處理結果。對於錯誤信息給用戶輸出有好的提示信息,並以非零錯誤process::exit(1)
退出命令行。
use std::{env, fs, process};
fn main(){
let config = Config::build(&args).unwrap_or_else(|err| {
println!("error occurred parseing args:{err}");
process::exit(1);
});
// ...
}
unwrap_or_else
可以進行自定義錯誤處理。這是一個閉包,它調用內部的匿名函數,並通過|err|
傳遞的參數供內部使用。當返回Ok
時,則返回內部的值。
提取讀取文件的邏輯
參數的取參邏輯經由結構體內部方法處理。現在吧文件讀取的邏輯提取出來,並採用傳遞錯誤的方式Result
返回錯誤信息。
use std::error::Error;
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let content = fs::read_to_string(config.file_path)?;
println!("read the content:\n{content}");
Ok(())
}
使用了 trait 對象 Box<dyn Error>
返回實現Error
trait 的類型,不用指定具體的錯誤類型。靈活性更高dyn
表示動態的
接著可以在主函數中調用run()
函數,並處理可能出現的錯誤。
fn main (){
// ...
if let Err(e) = run(config) {
println!("something error:{e}");
process::exit(1);
}
}
拆分代碼到庫
以上定義了結構體,處理取參函數;拆離了讀取文件邏輯。但是這些都是在src/main.rs
中,有複雜邏輯時,這會讓文件行數很多,看起來很讓人頭疼。
將這一部分拆離的放到其他文件中去。新建src/lib.rs
,將這些定義移動到該文件中。
use std::error::Error;
use std::fs;
pub struct Config {
pub search: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("至少傳入2個參數");
}
let search = args[1].clone();
let file_path = args[2].clone();
Ok(Config { search, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let content = fs::read_to_string(config.file_path)?;
println!("read the content:\n{content}");
Ok(())
}
可以看到通過pub
將這些結構體、函數都公有化。包括結構里的欄位,這就是一個可以測試的公有 API 的 crate 庫。
然後再src/main.rs
需要導入
use ifun_grep::{run, Config};
通過use
引入作用域。ifun-grep
是項目名稱,作為首碼。
增加測試
通過測試驅動開發的模式來逐漸增加邏輯。期望從給定的內容中查找出字元串,並列印出所在行。
在src/lib.rs
增加測試示例
#[cfg(test)]
mod test {
use super::*;
#[test]
fn on_result() {
let search = "hboot";
let content = "\
nice. rust
I'm hboot.
hello world.
";
assert_eq!(vec!["I'm hboot."], find(search, content));
}
}
搜索字元串hboot
,它在文本的第二行。所以期待搜索輸出結果為I'm hboot.
。
提供一個find
函數,用於處理搜索邏輯,先不寫搜索邏輯,返回一個空的結果值。
pub fn find<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
vec![]
}
利用顯示生命周期'a
來表明參數content
參數與返回值的生命周期相關聯。它們存在的時間一樣久
執行測試cargo test
,理所應當的輸出失敗,結果返回了一個空的vec![]
,和預期不匹配。
增加搜索邏輯,按行執行過濾,包含指定的字元串,則存儲在結果中。
pub fn find<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
let mut result = vec![];
for line in content.lines() {
if line.contains(search) {
// 符合,包含了指定字元串
result.push(line);
}
}
result
}
通過迭代器遍歷給定文本內容lines()
.字元串判斷是否包含contains()
方法。將結果值放進result
中,並返回。
測試用例測試沒有問題,完善一下run
函數,搜索出符合的內容並列印出來
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let content = fs::read_to_string(config.file_path)?;
// println!("read the content:\n{content}");
for line in find(&config.search, &content) {
println!("{line}");
}
Ok(())
}
執行腳本cargo run -- 山 hello.txt
,可以看到列印輸出兩行
增加環境變數
功能已經到到預期,可以搜索出想要包含字元串的文本段落。增加一個額外的功能大小寫敏感
處理環境變數,當然也可以通過再多傳一個參數處理。
更改文本內容為應為
Let life be beautiful like summer flowers.
The world has kissed my soul with its pain.
Eyes are raining for her.
you also miss the stars.
先測試當前程式是否大小寫敏感,文本中首個英文單詞是大寫的,按照小寫搜索
$> cargo run -- let hello.txt
沒有任何的列印輸出,說明當前的搜索邏輯是大小寫敏感的,通過傳遞變數來控制邏輯,修改測試用例,增加兩個測試示例:大小寫敏感和不敏感測試。
#[cfg(test)]
mod test {
use super::*;
#[test]
fn case_sensitive() {
let search = "rust";
let content = "\
nice. rust
I'm hboot.
hello world.
Rust
";
assert_eq!(vec!["nice. rust"], find(search, content));
}
#[test]
fn case_insensitive() {
let search = "rust";
let content = "\
nice. rust
I'm hboot.
hello world.
Rust
";
assert_eq!(vec!["nice. rust", "Rust"], find_insensitive(search, content));
}
}
原來的函數find
大小寫敏感,邏輯不變。增加一個大小寫不敏感的函數find_insensitive
,在處理搜索時,查詢的字元和被搜索的文本行都轉小寫後,然後在執行查找。
pub fn find_insensitive<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
let mut result = vec![];
// 搜索 字元串轉小寫
let search = search.to_lowercase();
for line in content.lines() {
// 文本行內容轉小寫
if line.to_lowercase().contains(&search) {
// 符合,包含了指定字元串
result.push(line);
}
}
result
}
多了一個操作to_lowercase()
將文本內容轉成小寫。to_lowercase()
會新創建一個 String,contains()
方法參數需要的是一個引用。
再次執行測試cargo teset
.用例全部通過。邏輯寫好了,需要通過增加一個配置來處理是否大小寫敏感。
修改結構體定義ingore_case
表示來忽略大小寫。
pub struct Config {
pub search: String,
pub file_path: String,
pub ignore_case: bool,
}
通過ingore_case
欄位判斷是否調用哪個函數,修改run
函數
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let content = fs::read_to_string(config.file_path)?;
// println!("read the content:\n{content}");
let mut result = vec![];
if config.ignore_case {
result = find_insensitive(&config.search, &content)
} else {
result = find(&config.search, &content)
}
for line in result {
println!("{line}");
}
Ok(())
}
處理接受變數IGNORE_CASE
,通過庫std::env
處理環境變數。
impl Config {
pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("至少傳入2個參數");
}
let search = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
search,
file_path,
ignore_case,
})
}
}
env::var()
返回值為 Result 類型,通過它自己的方法is_ok()
判斷什麼狀態,如果設置值則返回 true;未設置則返回 false。
進行測試,不設置變數時,查詢小寫的let
是查詢不到的,因為首寫的因為單詞字母是大些的。
$> cargo run -- let hello.txt
通過設置環境變數,執行程式
$> IGNORE_CASE=1 cargo run -- let hello.txt
可以查到目標文本內容。
錯誤信息處理
我們所預先知道的錯誤信息都通過程式執行println!
列印在控制台,這是一種標準輸出.
對於出現錯誤信息,希望它即時列印輸出,而對於程式執行的結果記錄下來,保存到文件中,方便查看。
現在使用println!
標準輸出流重定向到文件中,它會將錯誤信息也保存到起來,且不會列印。
$> cargo run >output.txt
屏幕上沒有任何輸出,以為程式執行正常,其實文件中的內容是error occurred parseing args:至少傳入2個參數
。
這就造成了一個問題,不管成功、失敗,只有打開文件才能看到。錯誤輸出使用標準錯誤展示用於錯誤信息,將錯誤列印的println!
改為eprintln!
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
// println!("error occurred parseing args:{err}");
eprintln!("error occurred parseing args:{err}");
process::exit(1);
});
println!("will search {} in {}", config.search, config.file_path);
if let Err(e) = run(config) {
// println!("something error:{e}");
eprintln!("something error:{e}");
process::exit(1);
}
}
重新執行cargo run >output.txt
,錯誤列印到控制台,而文件output.txt
沒有輸出。
再執行,可以查到數據的命令cargo run -- Let hello.txt > output.txt
,查看output.txt
,可以看到預期的查找到的內容在文件中。
發佈 crate 到Crate.io
crates.io 庫,可以這裡找找想要的功能庫,也可以將自己的 crate 發佈到這裡。
Rust 的發佈配置都有一套預設的、可定製的配置。
cargo build
採用的是 dev 配置構建程式cargo build --release
是 release 配置,有更好的發佈構建的配置
可以在文件Cargo.toml
中通過[profile.*]
修改設置預設值。
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
dev 構建和發佈構建定義不同的優化等級。opt-level
定義何種程度優化,0-3
可配置值。dev 預設為 0,release 預設為 3.
如果想 dev 模式下需要一些優化,則可以更改為
[profile.dev]
opt-level = 1
增加文檔註釋
一個好的模塊包,是有很好的文檔說明,以方便其他人輕易上手。通過文檔註釋///
已支持 markdown 格式化文本。
給每一個函數增加註釋說明,這裡只展示部分。
/// the struct `Config` defines command line params.
///
/// # Example
///
/// ```
/// let search = String::from("let");
/// let config = ifun_grep::Config {
/// search,
/// file_path:String::from("hello.txt"),
/// ignore_case:false,
/// };
///
/// ```
pub struct Config {
pub search: String,
pub file_path: String,
pub ignore_case: bool,
}
/// the fun is used to execute search
///
/// # example
/// ```
/// let search = String::from("let");
/// let config = ifun_grep::Config {
/// search,
/// file_path:String::from("hello.txt"),
/// ignore_case:false,
/// };
///
/// let result = ifun_grep::run(config);
///
/// assert!(result.is_ok());
/// ```
///
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let content = fs::read_to_string(config.file_path)?;
// println!("read the content:\n{content}");
let result;
if config.ignore_case {
result = find_insensitive(&config.search, &content);
} else {
result = find(&config.search, &content);
}
for line in result {
println!("{line}");
}
Ok(())
}
在使用 vscode 時,註釋文檔上方會有一個執行操作 run doctest
。可以單獨執行當前寫的測試示例是否可以通過執行。
也可以通過cargo test
來測試所有的測試示例。不僅會執行mod test
的測試示例,也會執行doc test
的註釋測試示例。
通過命令cargo doc --open
來生成線上文檔。
$> cargo doc --open
可以通過//!
對當前文件進行註釋說明,必須是在第一行。
//! ifun_grep is a string search library
//!
//! Supports case sensitive search.
//!
註冊 crate.io
賬戶併發布
目前只能使用 github 賬號進行授權登錄。在個人賬號信息中,API Tokens
生成 token 授權操作。
$> cargo login 你的token
如果登錄不成功,看下提示錯誤,我是加了參數--registry crates-io
才成功的。
$> cargo login 你的token --registry crates-io
登錄之後就可以發佈了,通過Cargo.toml
增加一些倉庫元信息,比如倉庫名、作者、開源協議、描述等等。
$> cargo publish
發佈之前需要驗證你登錄的賬號郵箱,不然發佈不了。個人的元信息有幾項是必填的,包括name\version\description\license
發佈時,如果發佈不成功,看錯誤提示,可能還需要加--registry crates-io
撤銷某個版本
如果你發佈的版本有很大的問題,可以撤銷改版本。不能刪除倉庫,已發佈的代碼時永久存在的,只能通過撤銷來阻止其他項目引用它。
$> cargo yank --vers 0.1.0
使得當前版本不可用。也可以恢復當前版本的使用
$> cargo yank --vers 0.1.0 --undo
追逐的不應該是夢想,隨心所欲,隨遇而安!