這是上一篇 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 = "https://www.cnblogs.com/dreamHot/archive/2023/06/18/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=1panics和 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/555456.html
標籤:其他
上一篇:[ARM 匯編]進階篇—例外處理與中斷—2.4.2 ARM處理器的例外向量表
下一篇:返回列表
