- 原文地址:Your first CLI tool with Rust
- 原文做者:Jérémie Veillet
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:JackEggie
- 校對者:TloveYing
在精彩的編程世界裏,你可能據說過這種名爲 Rust 的新語言。它是一種開源的系統級編程語言。它專一於性能、內存安全和並行性。你能夠像 C/C++ 那樣用它編寫底層應用程序。html
你可能已經在 Web Assembly 網站上見到過它了。Rust 可以編譯 WASM 應用程序,你能夠在 Web Assembly FAQ 上找到不少例子。它也被認爲是 servo 的基石,servo 是一個在 Firefox 中實現的高性能瀏覽器引擎。前端
這可能會讓你望而卻步,但這不是咱們要在這裏討論的內容。咱們將介紹如何使用它構建命令行工具,而你可能會從中發現不少有意思的東西。android
好吧,讓我把事情說清楚。我本能夠用任何其餘語言或框架來完成命令行工具。我能夠選 C、Go、Ruby 等等。甚至,我可使用經典的 bash。ios
在 2018 年中,我想學習一些新東西,Rust 激發了個人好奇心,同時我也須要構建一些簡單的小工具來自動化工做和我的項目中的一些流程。git
你可使用 Rustup 來設置你的開發環境,它是安裝和配置你機器上全部的 Rust 工具的主要入口。github
若是你在 Linux 和 MacOS 上工做,使用以下命令便可完成安裝:web
$ curl <https://sh.rustup.rs> -sSf | sh
複製代碼
若是你使用的是 Windows 系統,一樣地,你須要在 Rustup 網站上下載一個 exe
並運行。編程
若是你用的是 Windows 10,我建議你使用 WSL 來完成安裝。以上就是安裝所需的步驟,咱們如今能夠去建立咱們的第一個 Rust 應用程序了!json
咱們在這裏要作的是,仿照 cat 來構建一個 UNIX 實用工具,或者至少是一個簡化版本,咱們稱之爲 kt
。這個應用程序將接受一個文件路徑做爲輸入,並在終端的標準輸出中顯示文件的內容。windows
要建立這個應用程序的基本框架,咱們將使用一個名爲 Cargo 的工具。它是 Rust 的包管理器,能夠將它看做是 Rust 工具的 NPM(對於 Javascript 開發者)或 Bundler(對於 Ruby 開發者)。
打開你的終端,進入你想要存儲源代碼的路徑下,而後輸入下面的代碼。
$ cargo init kt
複製代碼
這將會建立一個名爲 kt
的目錄,該目錄下已經有咱們應用程序的基本結構了。
若是咱們 cd
到該目錄中,咱們將看到這個目錄結構。並且,方便的是,這個項目已經默認初始化了 git。真是太好了!
$ cd kt/
|
.git/
|
.gitignore
|
Cargo.toml
|
src/
複製代碼
Cargo.toml
文件包含了咱們的應用程序的基本信息和依賴信息。一樣地,能夠把它看作應用程序的 package.json
或者 Gemfile
文件。
src/
目錄包含了應用程序的源文件,咱們能夠看到其中只有一個 main.rs
文件。檢查文件的內容,咱們能夠看到其中只有一個 main
函數。
fn main() {
println!("Hello, world!");
}
複製代碼
試試構建這個項目。因爲沒有外部依賴,它應該會構建得很是快。
$ cargo build
Compiling kt v0.1.0 (/Users/jeremie/Development/kitty)
Finished dev [unoptimized + debuginfo] target(s) in 2.82s
複製代碼
在開發模式下,你能夠經過調用 cargo run
來執行二進制文件(用 cargo run --- my_arg
來傳遞命令行參數)。
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/kt`
Hello, world!
複製代碼
恭喜你,你經過剛纔的步驟已經建立並運行了你的第一個 Rust 應用程序了!🎉
正如我以前在文章中所說的,咱們正在嘗試構建一個簡化版的 cat
命令。咱們的目標是模擬 cat
的行爲,運行 kt myfile.txt
命令以後,在終端輸出文件內容。
咱們原本能夠本身處理參數的解析過程,但幸運的是,一個 Rust 工具能夠幫咱們簡化這個過程,它就是 Clap。
這是一個高性能的命令行參數解析器,它讓咱們管理命令行參數變得很簡單。
使用這個工具的第一步是打開 Cargo.toml
文件,並在其中添加指定的依賴項。若是你從未處理過 .toml
文件也不要緊,它與 Windows 系統中的 .INI
文件極其類似。這種文件格式在 Rust 中是很常見的。
在這個文件中,你將看到有一些信息已經填充好了,好比做者、版本等等。咱們只須要在 [dependencies]
下添加依賴項就好了。
[dependencies]
clap = "~2.32"
複製代碼
保存文件後,咱們須要從新構建項目,以便可以使用依賴庫。即便 cargo
下載了除 clap
之外的文件也不用擔憂,這是因爲 clap
也有其所需的依賴關係。
$ cargo build
Updating crates.io index
Downloaded clap v2.32.0
Downloaded atty v0.2.11
Downloaded bitflags v1.0.4
Downloaded ansi_term v0.11.0
Downloaded vec_map v0.8.1
Downloaded textwrap v0.10.0
Downloaded libc v0.2.48
Downloaded unicode-width v0.1.5
Downloaded strsim v0.7.0
Compiling libc v0.2.48
Compiling unicode-width v0.1.5
Compiling strsim v0.7.0
Compiling bitflags v1.0.4
Compiling ansi_term v0.11.0
Compiling vec_map v0.8.1
Compiling textwrap v0.10.0
Compiling atty v0.2.11
Compiling clap v2.32.0
Compiling kt v0.1.0 (/home/jeremie/Development/kt)
Finished dev [unoptimized + debuginfo] target(s) in 33.92s
複製代碼
以上就是須要配置的內容,接下來咱們能夠動手,寫一些代碼來讀取咱們的第一個命令行參數。
打開 main.rs
文件。咱們必須顯式地聲明咱們要使用 Clap 庫。
extern crate clap;
use clap::{Arg, App};
fn main() {}
複製代碼
extern crate
關鍵字用於導入依賴庫,你只需將其添加到主文件中,應用程序的任何源文件就均可以引用它了。use
部分則是指你將在這個文件中使用 clap
的哪一個模塊。
Rust 模塊(module)的簡要說明:
Rust 有一個模塊系統,可以以有組織的方式重用代碼。模塊是一個包含函數或類型定義的命名空間,你能夠選擇這些定義是否在其模塊外部可見(public/private)。—— Rust 文檔
這裏咱們聲明的是咱們想要使用 Arg
和 App
模塊。咱們但願咱們的應用程序有一個 FILE
參數,它將包含一個文件路徑。Clap 能夠幫助咱們快速實現該功能。這裏使用了一種鏈式調用方法的方式,這是一種使人很是愉悅的方式。
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
}
複製代碼
再次編譯並執行,除了變量 matches
上的編譯警告(對於 Ruby 開發者來講,能夠在變量前面加上 _
,它會告訴編譯器該變量是可選的),它應該不會輸出太多其餘信息。
若是你嚮應用程序傳遞 -h
或者 -V
參數,程序會自動生成一個幫助信息和版本信息。我不知道你如何看待這個事情,但我以爲它 🔥🔥🔥。
$ cargo run -- -h
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/kt -h`
kt 0.1.0
Jérémie Veillet. jeremie@example.com
A drop-in cat replacement written in Rust
USAGE:
kt [FILE]
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
ARGS:
<FILE> File to print.
$ cargo run --- -V
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running target/debug/kt -V
kt 0.1.0
複製代碼
咱們還能夠嘗試不帶任何參數,啓動程序,看看會發生什麼。
$ cargo run --
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/kt`
複製代碼
什麼都沒有發生。這是每次構建命令行工具時應該發生的默認行爲。我認爲不向應用程序傳遞任何參數就永遠不該該觸發任何操做。即便有時候這並不正確,可是在大多數狀況下,永遠不要執行用戶從未打算執行的操做。
如今咱們已經有了參數,咱們能夠深刻研究如何捕獲這個命令行參數並在標準輸出中顯示一些內容。
要實現這一點,咱們可使用 clap
中的 value_of
方法。請參考文檔來了解該方法是怎麼運做的。
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
println!("Value for file argument: {}", file);
}
}
複製代碼
此時,你能夠運行應用程序並傳入一個隨機字符串做爲參數,在你的控制檯中會回顯該字符串。
$ cargo run -- test.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/kt test.txt`
Value for file argument: test.txt
複製代碼
請注意,目前咱們實際上沒有對該文件是否存在進行驗證。那麼咱們應該怎麼實現呢?
有一個標準庫可讓咱們檢查一個文件或目錄是否存在,使用方式很是簡單。它就是 std::path
庫。它有一個 exists
方法,能夠幫咱們檢查文件是否存在。
如前所述,使用 use
關鍵字來添加依賴庫,而後編寫以下代碼。你能夠看到,咱們使用 If-Else
條件控制在輸出中打印一些文本。println!
方法會寫入標準輸出 stdout
,而 eprintln!
會寫入標準錯誤輸出 stderr
。
extern crate clap;
use clap::{Arg, App};
use std::path::Path;
use std::process;
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
println!("Value for file argument: {}", file);
if Path::new(&file).exists() {
println!("File exist!!");
}
else {
eprintln!("[kt Error] No such file or directory.");
process::exit(1); // 程序錯誤終止時的標準退出碼
}
}
}
複製代碼
咱們快要完成了!如今咱們須要讀取文件的內容並將結果顯示在 stdout
中。
一樣,咱們將使用一個名爲 File
的標準庫來讀取文件。咱們將使用 open
方法讀取文件的內容,而後將其寫入一個字符串對象,該對象將在 stdout
中顯示。
extern crate clap;
use clap::{Arg, App};
use std::path::Path;
use std::process;
use std::fs::File;
use std::io::{Read};
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
if Path::new(&file).exists() {
println!("File exist!!");
let mut f = File::open(file).expect("[kt Error] File not found.");
let mut data = String::new();
f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
println!("{}", data);
}
else {
eprintln!("[kt Error] No such file or directory.");
process::exit(1);
}
}
}
複製代碼
再次構建並運行此代碼。恭喜你!咱們如今有一個功能完整的工具了!🍾
$ cargo build
Compiling kt v0.1.0 (/home/jeremie/Development/kt)
Finished dev [unoptimized + debuginfo] target(s) in 0.70s
$ cargo run -- ./src/main.rs
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/kt ./src/main.rs`
File exist!!
extern crate clap;
use clap::{Arg, App};
use std::path::Path;
use std::process;
use std::fs::File;
use std::io::{Read};
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
if Path::new(&file).exists() {
println!("File exist!!");
let mut f = File::open(file).expect("[kt Error] File not found.");
let mut data = String::new();
f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
println!("{}", data);
}
else {
eprintln!("[kt Error] No such file or directory.");
process::exit(1);
}
}
}
複製代碼
咱們的應用程序現能夠接收一個參數並在 stdout
中顯示結果。
咱們能夠稍微調整一下整個打印階段的性能,方法是用 writeln!
來代替 println!
。這在 Rust 輸出教程中有很好的解釋。在此過程當中,咱們能夠清理一些代碼,刪除沒必要要的打印,並對可能的錯誤場景進行微調。
extern crate clap;
use clap::{Arg, App};
use std::path::Path;
use std::process;
use std::fs::File;
use std::io::{Read, Write};
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
if Path::new(&file).exists() {
match File::open(file) {
Ok(mut f) => {
let mut data = String::new();
f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
let stdout = std::io::stdout(); // 獲取全局 stdout 對象
let mut handle = std::io::BufWriter::new(stdout); // 可選項:將 handle 包裝在緩衝區中
match writeln!(handle, "{}", data) {
Ok(_res) => {},
Err(err) => {
eprintln!("[kt Error] Unable to display the file contents. {:?}", err);
process::exit(1);
},
}
}
Err(err) => {
eprintln!("[kt Error] Unable to read the file. {:?}", err);
process::exit(1);
},
}
}
else {
eprintln!("[kt Error] No such file or directory.");
process::exit(1);
}
}
}
複製代碼
$ cargo run -- ./src/main.rs
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/kt ./src/main.rs`
extern crate clap;
use clap::{Arg, App};
use std::path::Path;
use std::process;
use std::fs::File;
use std::io::{Read, Write};
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
if Path::new(&file).exists() {
match File::open(file) {
Ok(mut f) => {
let mut data = String::new();
f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
let stdout = std::io::stdout(); // 獲取全局 stdout 對象
let mut handle = std::io::BufWriter::new(stdout); // 可選項:將 handle 包裝在緩衝區中
match writeln!(handle, "{}", data) {
Ok(_res) => {},
Err(err) => {
eprintln!("[kt Error] Unable to display the file contents. {:?}", err);
process::exit(1);
},
}
}
Err(err) => {
eprintln!("[kt Error] Unable to read the file. {:?}", err);
process::exit(1);
},
}
}
else {
eprintln!("[kt Error] No such file or directory.");
process::exit(1);
}
}
}
複製代碼
咱們完成了!咱們經過約 45 行代碼就完成了咱們的簡化版 cat
命令 🤡,而且它表現得很是好!
那麼構建這個應用程序並將其安裝到文件系統中要怎麼作呢?向 cargo 尋求幫助吧!
cargo build
接受一個 ---release
標誌位,以便咱們能夠指定咱們想要的可執行文件的最終版本。
$ cargo build --release
Compiling libc v0.2.48
Compiling unicode-width v0.1.5
Compiling ansi_term v0.11.0
Compiling bitflags v1.0.4
Compiling vec_map v0.8.1
Compiling strsim v0.7.0
Compiling textwrap v0.10.0
Compiling atty v0.2.11
Compiling clap v2.32.0
Compiling kt v0.1.0 (/home/jeremie/Development/kt)
Finished release [optimized] target(s) in 28.17s
複製代碼
生成的可執行文件位於該子目錄中:./target/release/kt
。
你能夠將這個文件複製到你的 PATH
環境變量中,或者使用一個 cargo 命令來自動安裝。應用程序將安裝在 ~/.cargo/bin/
目錄中(確保該目錄在 ~/.bashrc
或 ~/.zshrc
的 PATH
環境變量中)。
$ cargo install --path .
Installing kt v0.1.0 (/home/jeremie/Development/kt)
Finished release [optimized] target(s) in 0.03s
Installing /home/jeremie/.cargo/bin/kt
複製代碼
如今咱們能夠直接在終端中使用 kt
命令調用咱們的應用程序了!\o/
$ kt -V
kt 0.1.0
複製代碼
咱們建立了一個僅有數行 Rust 代碼的命令行小工具,它接受一個文件路徑做爲輸入,並在 stdout
中顯示該文件的內容。
你能夠在這個 GitHub 倉庫中找到這篇文章中的全部源代碼。
輪到你來改進這個工具了!
-n
選項)。ENTER
鍵來顯示其他部分。kt myfile.txt myfile2.txt myfile3.txt
這樣的語法一次性打開多個文件。不要猶豫,告訴我你用它作了什麼!😎
特別感謝幫助修訂這篇文章的 Anaïs 👍
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。