這是上一篇 [rust 學習 - 構建 mini 命令行工具](https://www.cnblogs.com/dreamHot/p/17467837.html)的續作,擴展增加一些 crate 庫。這些基礎庫在以後的編程工作中會常用到,他們作為基架存在於項目中,解決項目中的某個問題。 項目示例還是 ...
這是上一篇 rust 學習 - 構建 mini 命令行工具的續作,擴展增加一些 crate 庫。這些基礎庫在以後的編程工作中會常用到,他們作為基架存在於項目中,解決項目中的某個問題。
項目示例還是以上一篇的工程為基礎做調整修改ifun-grep 倉庫地址
怎麼去使用已發佈的 crate 庫
在開發ifun-grep
項目時,運行項目命令為cargo run -- hboot hello.txt
,測試項目的邏輯正確。在發佈到crates.io
要如何使用呢,
在項目中使用
作為項目的一個功能函數,邏輯事務調用。在crates.io 中找到需要的庫
安裝已經發佈的示例庫ifun-grep
. 通過cargo add
添加依賴項
這裡我們有一個測試示例項目rust-web
,這是在另一篇rust 基礎中創建的示例項目。
$> cargo add ifun-grep
安裝成功後,可以在在項目的Cargo.toml
看到依賴
[dependencies]
ifun-grep = "0.1.0"
在main.rs
導入庫使用,這個庫包括了一個結構體Config
,三個方法find\find_insensitive\run
use ifun_grep;
fn main(){
let search = String::from("let");
let config = ifun_grep::Config {
search,
file_path: String::from("hello.txt"),
ignore_case: true,
};
let result = ifun_grep::run(config);
println!("{}", result.is_ok());
}
執行cargo run
,可以看到輸出了false
。因為文件hello.txt
不存在,在上一篇文中我們把錯誤處理統一放到了main.rs
文件中處理的。而我們這邊作為一個 lib 庫,直接調用的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.
再次運行,可以看到列印的輸出內容。Let life be beautiful like summer flowers.
可以通過cargo remove ifun-grep
從Cargo.toml
移除依賴
作為腳本命令執行
可以看到作為功能性函數調用時,只能手動去初始化函數調用。不能像執行命令一樣,傳遞參數調用,也就不能執行main.rs
中的處理邏輯以及錯誤列印。
通過cargo install
安裝二進位可執行文件的庫
$> cargo install ifun-grep
安裝完成後,就可以在全局環境中使用命令ifun-grep
了。
通過cargo uninstall ifun-grep
移除。
開發時如何測試使用
開發時只能cargo run
去執行main.rs
文件,不能直接使用ifun-grep
命令
可以通過cargo build
構建編譯,在target/debug
下生成二進位文件
這樣可以通過相對目錄地址訪問可執行文件執行命令
$> target/debug/ifun-grep Let hello.txt
如果我們的代碼 存儲在 github 或者 gitee 上,就可以將編譯包壓縮發佈版本,這樣需要的人不需要 cargo 就可以下載安裝。
構建發佈版本
$> cargo build --release
我的代碼倉庫在 giteeifun-grep 基礎版本發佈
下載壓縮包後,需要把可執行文件配置到系統環境中,全局可用。也可以不用配置,直接使用文件路徑地址執行命令。
還需要考慮一個問題,就是系統的相容性,mac、windows、linux 等等,想要發佈一個相容的庫,可能還需要針對性構建編譯包併發布
這裡演示的是 mac 系統下載發佈包後,通過路徑訪問執行命令
clap
庫解析 cli 參數
clap
庫包含了對子命令、shell 完成和更好的幫助信息。
安裝,參數--features
表示啟動特定功能,
$> cargo add clap --features derive
clap
除了提供基礎的功能之外,還可以通過--features
開啟特定功能。derive
啟動自定義派生功能,可以通過過程巨集處理參數。
在src/main.rs
中使用
// use std::{env, process};
use clap::Parser;
use ifun_grep::{Config};
fn main() {
// let args: Vec<String> = env::args().collect();
let config = Config::parse();
// let config = Config::build(&args).unwrap_or_else(|err| {
// // println!("error occurred parseing args:{err}");
// eprintln!("error occurred parseing args:{err}");
// process::exit(1);
// });
}
結構體 Config
派生了一個內部函數parse
,可以直接解析參數生成實例 config
。
還需要修改src/lib.rs
,使得結構體 Config
用擁有這種能力
use clap::Parser;
#[derive(Parser, Debug)]
pub struct Config {
#[arg(long)]
pub search: String,
#[arg(long)]
pub file_path: String,
#[arg(long)]
pub ignore_case: bool,
}
首先不再使用std::env
去解析 cli 參數,也不需要調用Config
的 build 方法去實例化創建 config。
通過clap::Parser
的過程式巨集 Parser
去解析 cli 參數,並返回結構體Config
的實例 config
執行命令
$> cargo run
報錯了,如圖,首先這個錯誤信息很友好,告訴我們必填的參數信息
增加參數配置,調用命令執行
$> cargo run -- --search Let --file-path hello.txt
可以看到結果成功了,對比之前調用方式cargo run -- Let hello.txt
,多了一個參數名稱定義--search
#[arg(long)]
參數巨集是用來定義參數接受的標誌,arg
還有許多其他的功能
移除掉#[arg(long)]
,執行命令 cargo run
use clap::Parser;
#[derive(Parser, Debug)]
pub struct Config {
pub search: String,
pub file_path: String,
pub ignore_case: bool,
}
報錯了,thread 'main' panicked at 'Argument 'ignore_case is positional, it must take a value
,意思是 ignore_case 必須要有一個值,ignore_case
是一個布爾值,隱式啟動了#[arg(action = ArgAction::SetTrue)]
,所以需要設置接受標誌
布爾值只需要通過設置標誌,而不需要設置值,--ignore_case
就表示 true
use clap::Parser;
pub struct Config {
pub search: String,
pub file_path: String,
#[arg(short, long)]
pub ignore_case: bool,
}
再次執行命令cargo run
,
還需要必填的兩個參數,此時不需要--
了
$> cargo run -- Let hello.txt
要想開啟大小寫不敏感,則需要增加--ignore-case
$> cargo run -- let hello.txt --ignore-case
需要註意的是結構體定義的下劃線ignore_case
,在 clap 接受參數的標誌為--ignore-case
增加命令的描述信息
通常 cli 的命令都有一個--help
功能,這可以基本說明這個腳本是幹嘛的,以及怎麼去使用
而這些 clap 正好有。測試一下,代碼修改後需執行cargo build
$> target/debug/ifun-grep --help
可以看到對於ifun-grep
一個基本的使用方式,包括Usage、Arguments、Options
。還展示了對於結構體Config
的註釋說明、例子。
通過簡寫的-h
可以讓描述更加緊湊一點。
clap 通過#[command()]
可以從Cargo.toml
獲取到一些基礎信息,生成 Command 實例
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub struct Config {
pub search: String,
pub file_path: String,
#[arg(short, long)]
pub ignore_case: bool,
}
編譯後,執行--help
也可以自定義這些欄位的值。
#[derive(Parser)]
#[command(name = "ifun-grep")]
#[command(author = "hboot <[email protected]>")]
#[command(version = "0.2.0")]
#[command(about="A simple fake grep",long_about=None)]
pub struct Config {
pub search: String,
pub file_path: String,
#[arg(short, long)]
pub ignore_case: bool,
}
通過執行ifun-grep -V
可以查看設置的name、version
信息
定義參數非必須
通過Option
定義欄位數據類型,使得這個欄位非必須
#[derive(Parser)]
pub struct Config {
name: Option<String>,
}
通過--help
查看參數是,必須的Arguments:
參數是<SEARCH>
使用尖括弧的;而非必須的是中括弧[name]
.
如果某個參數可以接受多個,則通過集合定義類型
#[derive(Parser)]
pub struct Config {
name: Vec<String>,
}
在命令執行多餘的參數會解析到欄位 name 中。隱式的啟動了#[arg(action = ArgAction::Set)]
,處理多個值。
使用標誌命名參數
在之前的實例中,已經使用了#[arg(short, long)]
,它用來標識參數名稱,它可以:
- 意圖表達更明確
- 不用在意參數的順序
- 使參數變的可選
#[derive(Parser)]
pub struct Config {
#[arg(short, long)]
pub search: String,
#[arg(short, long)]
pub file_path: String,
#[arg(short, long)]
pub ignore_case: bool,
}
可以通過--help
查看變化,所有的參數都變成了Options
子命令
在執行ifun-grep
時,攜帶子命令執行。通過#[derive(Subcommand)]
標誌屬性,子命令也可以有自己的版本、作者信息、參數等等
use clap::{Args, Parser, Subcommand};
#[derive(Parser)]
pub struct Config {
#[arg(short, long)]
pub search: String,
#[arg(short, long)]
pub file_path: String,
#[arg(short, long)]
pub ignore_case: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
Add(AddArgs),
}
#[derive(Args)]
pub struct AddArgs {
name: Option<String>,
}
預設值
可以通過#[arg(default_value_t)]
定義預設值,定義欄位file_path
預設值
#[derive(Parser)]
pub struct Config {
#[arg(short, long)]
pub search: String,
#[arg(short, long, default_value = "hello.txt")]
pub file_path: String,
#[arg(short, long)]
pub ignore_case: bool,
}
調用命令執行時,可以不在設置該欄位
$> target/debug/ifun-grep -s Let
命令執行是查詢成功的.
其他的功能比如:數據校驗、自定義值解析邏輯、自定義校驗等等。
anyhow
處理錯誤
提供了一種錯誤類型anyhow::Error
. 處理出現的錯誤。
之前處理文件讀取的邏輯,使用了?
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let content = fs::read_to_string(config.file_path)?;
Ok(())
}
當文件不存在時,會列印出錯誤信息something error:No such file or directory (os error 2)
.但不知道具體哪個文件不存在。
可以通過自定義錯誤類型IfunError
,來構建自己的錯誤信息
#[derive(Debug)]
pub struct IfunError(String);
pub fn run(config: Config) -> Result<(), IfunError> {
let file_path = config.file_path.clone();
let content = fs::read_to_string(config.file_path)
.map_err(|err| IfunError(format!("could not read file {} - {}", file_path, err)))?;
// ...
Ok(())
}
再次執行訪問不存在的文件,報錯信息為something error:IfunError("could not read file 1.txt - No such file or directory (os error 2)")
而anyhow
正好做了事情,可以通過 anyhow 的特征context
可以附加錯誤內容信息,也保留了原始錯誤。
安裝 crate anyhow
庫
$> cargo add anyhow
調整處理讀取文件的函數run
,在src/lib.rs
文件中修改:
use anyhow::{Context, Result};
pub fn run(config: Config) -> Result<()> {
let file_path = config.file_path.clone();
let content = fs::read_to_string(config.file_path)
.with_context(|| format!("could not read file {}", file_path))?;
// ...
Ok(())
}
還需要修改src/main.rs
文件,將錯誤輸出方式改為{:?}
fn main() {
// ...
if let Err(e) = run(config) {
// println!("something error:{e}");
eprintln!("something error:{:?}", e);
process::exit(1);
}
}
再次執行命令,可以看到錯誤更加的友好。
使用anyhow!()
巨集,輸出錯誤信息
在src/main.rs
,讀取文件之前增加錯誤輸出
fn main(){
// ...
println!("{}", anyhow!("anyhow error {}", "running"));
//...
}
使用bail!()
巨集,中斷執行
調用執行返回錯誤,中斷程式執行
在src/main.rs
,讀取文件之前增加錯誤輸出
use anyhow::{anyhow, bail};
fn main() -> Result<(), anyhow::Error> {
// ...
println!("{}", anyhow!("anyhow error {}", "running"));
bail!("permission denied for accessing {}", config.file_path)
//...
}
調用bail!
時,返回值必須是Result<(), anyhow::Error>
類型
bail!
同等於return Err(anyhow!())
跟蹤錯誤棧
列印出錯誤信息,我們可以知道發生了錯誤,想知道是哪個文件、那行代碼發生的錯誤,則需要開啟錯誤棧追蹤。
這是一個特性功能,需要指定特性啟用
$> cargo add anyhow --features backtrace
然後通過設置環境變數,
RUST_BACKTRACE=1
panics
和 error 都有錯誤棧輸出RUST_LIB_BACKTRACE=1
僅打開錯誤輸出RUST_BACKTRACE=1
和RUST_LIB_BACKTRACE=0
僅 panic 時
在執行命令時,設置環境變數,打開錯誤輸出時的錯誤追蹤
$> RUST_LIB_BACKTRACE=1 cargo run -- -s let -f 1.txt
thiserror
自定義自己的錯誤類型
與anyhow
不同,thiserror
可以用來自定義錯誤類型。
通過過程式巨集#[derive(Error)]
,它是由 std::error::Error
派生而來。
$> cargo add thiserror
定義一個文件不能存在的錯誤類型,並用於讀取文件時的邏輯
use thiserror::Error;
#[derive(Error, Debug)]
pub enum IfunError {
#[error("the file is't exist")]
FileNotExist(#[from] std::io::Error),
}
pub fn run(config: Config) -> Result<(), IfunError> {
let content = fs::read_to_string(config.file_path)?;
// ...
Ok(())
}
執行命令,訪問不存在的文件。錯誤信息輸出會被自定義的類型包裹:
ansi_term
更好的列印輸出
ansi_term
控制臺上的列印輸出,包括字體樣式、格式化。
安裝
$> cargo add ansi_term
包括對文本的字體顏色、背景色、是否加粗、是否閃爍等等。
通過ansi_term::Colour
控制字體樣式
我們將ifun-grep
的 參數列印使用顏色標記輸出
use ansi_term::Colour::{Green, Yellow};
fn main(){
//...
println!(
"will search {} in {}",
Green.paint(&config.search),
Yellow.paint(&config.file_path)
);
}
執行命令cargo run -- -s Let -f hello.txt
加粗bold()
、加下劃線underline()
、背景色on()
等
use ansi_term::Colour::{Green, Yellow};
fn main(){
//...
println!(
"will search {} in {}",
Green.bold().paint(&config.search),
Yellow.underline().paint(&config.file_path)
);
}
給程式查詢出的行數據加背景色、閃爍
use ansi_term::Colour::{Red, Yellow};
pub fn run(config: Config) -> Result<(), anyhow::Error> {
//...
for line in result {
println!("{}", Red.on(Yellow).blink().paint(line));
}
Ok(())
}
通過ansi_term::Style
控制樣式
Colour
是一個枚舉類型,專門針對顏色樣式處理;Style
是結構體類型,是字體樣式的集合。
設置字體顏色,結構體需要實例化一個實例對象,然後再調用對應的方法。
use ansi_term::Colour::{Green, Yellow};
use ansi_term::Style;
fn main(){
//...
println!(
"will search {} in {}",
Green.bold().paint(&config.search),
// Yellow.underline().paint(&config.file_path)
Style::new().fg(Yellow).paint(&config.file_path)
);
}
顏色擴展ansi_term::Colour::Fixed
除了內置枚舉的顏色,還可以通過色碼值設置顏色。0-255
use ansi_term::Colour::Fixed;
Fixed(154).paint("other color");
也可以通過ansi_term::Colour::RGB
,設置三個不同的值
use ansi_term::Colour::RGB;
RGB(154, 56, 178).paint("other color");
此外還有內置ANSIStrings
類型,可以通過to_string()
方法轉換為String
;
支持格式化輸出\[u8]
位元組字元串,對於不知道編碼的文本輸出很有用。會生成ANSIByteString
類型,通過write_to
方法寫入輸出流中。
Green.paint("ansi_term".as_bytes()).write_to(&mut std::io::stdout()).unwrap();
indicatif
展示進度條
處理任務時,顯示任務的執行進度。會讓人感覺良好,更有耐心等待執行完畢
$> cargo add indicatif
手動創建一個進度條,為了看到進度條的進度效果,可以使用std::thred
線程休眠一段時間。
use indicatif::ProgressBar;
use std::{thread, time};
fn main(){
let bar = ProgressBar::new(100);
let ten_millis = time::Duration::from_millis(10);
for _ in 0..100 {
bar.inc(1);
thread::sleep(ten_millis);
// ...
}
bar.finish();
}
通過ProgressBar
類型創建了一個進度條的實例對象,然後通過實例bar.inc()
逐步增加進度。完成後調用bar.finish()
表示進度完成,並保留顯示進度信息。
也支持多進度條的MultiProgress
log
日誌記錄
一個程式運行時期的日誌列印,非常重要,這對於運行監測喝解決有問題都有很到的幫助。
$> cargo add log
通常可以將日誌按照登記劃分,比如錯誤、警告、信息等。還需要一個日誌輸出的適配器 env_logger
,可以將日誌寫入終端、日誌伺服器等。
$> cargo add env_logger
美化輸出,將接受到的參數作為信息info!()
輸出,將產生的錯誤使用error!()
輸出
env_logger
預設輸出日誌到終端,
use log;
fn main() {
env_logger::init();
// ...
log::info!(
"will search {} in {}",
Green.bold().paint(&config.search),
Style::new().fg(Yellow).paint(&config.file_path)
);
// ...
if let Err(e) = run(config) {
log::error!("something error:{:?}", e);
process::exit(1);
}
}
必須在程式之前初始化完畢日誌環境變數配置。預設只展示error
錯誤類型的日誌
執行cargo run -- -s Let -f 1.txt
命令訪問不存在的文件,可以看到只有 error 錯誤輸出列印。
通過設置變數RUST_LOG=info
,查看
$> RUST_LOG=info cargo run -- -s Let -f 1.txt
初始化指定信息類型
在執行命令前加上RUST_LOG=info
很麻煩,有遺忘的可能,可以通過初始化env_logger::init()
調用時,設定一個預設值
use env_logger::Env;
fn mian(){
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
// ...
}
通過終端設置的變數優先順序比預設值高,可以通過執行時設置變數覆蓋預設值。
自定義輸出模板
可以看到預設的輸出列印包括了時間、類型以及模塊名。可以通過改變模板自定義輸出格式
use std::io::Write;
fn main(){
env_logger::builder()
.format(|buf, record| writeln!(buf, "{} - {}", record.level(), record.args()))
.init();
}
輸出格式改變為信息類型 - 信息
。使用預設的挺好,現在好多編輯器的日誌輸出都是這種格式。
測試
之前的單元測試示例都是和邏輯代碼放在一起的,並用#[test]
註釋。可以將這些測試放在tests
目錄中
新建tests/lib.rs
用於存放單元測試用例。
use ifun_grep::{find, find_insensitive};
#[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)
);
}
通過藉助第三飯庫來使得測試更容易,
assert_cmd
可以處理結果進行斷言;也可以測試調用命令進行測試。一起配合使用的還有predicates
用來斷言布爾值類型結果值
因為測試示例只在開發階段需要,則在安裝時加參數--dev
$> cargo add assert_cmd predicates --dev
新增一個處理文件不存在的的測試示例。日誌列印輸出時會包含有could not read file
字元串。
use assert_cmd::prelude::*;
use ifun_grep::{find, find_insensitive};
use predicates::prelude::*;
use std::{error::Error, process::Command};
#[test]
fn file_doesnt_exist() -> Result<(), Box<dyn Error>> {
let mut cmd = Command::cargo_bin("ifun-grep")?;
cmd.arg("-s let").arg("-f 1.txt");
cmd.assert()
.failure()
.stderr(predicate::str::contains("could not read file"));
Ok(())
}
通過運行cargo test
,測試示例是運行成功的。
assert_fs
用於測試文件系統的斷言
剛纔測試了文件不存在的錯誤輸出,還需要增加文件存在的測試,並寫入內容。
$> cargo add assert_fs --dev
生成要測試的文件;斷言測試生成的文件。tests/lib.rs
增加測試用例
use assert_cmd::prelude::*;
use assert_fs::prelude::*;
use ifun_grep::{find, find_insensitive};
use predicates::prelude::*;
use std::{error::Error, process::Command};
#[test]
fn file_content_exist() -> Result<(), Box<dyn Error>> {
let file = assert_fs::NamedTempFile::new("1.txt")?;
file.write_str("hello world \n Rust-web \n good luck for you!")?;
let mut cmd = Command::cargo_bin("ifun-grep")?;
cmd.arg("-s good").arg("-f").arg(file.path());
cmd.assert()
.success()
.stderr(predicate::str::contains("good luck for you!"));
Ok(())
}
這樣書寫的單元測試用例更能直接、明瞭。和實際使用ifun-grep
時同樣的命令操作,而不是使用開發時運行cargo run