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>回傳實作Errortrait 的型別,不用指定具體的錯誤型別,靈活性更高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
追逐的不應該是夢想,隨心所欲,隨遇而安!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/554674.html
標籤:其他
上一篇:如何吃透一個Java專案?
下一篇:返回列表
