用 Rust 打造你的第一個命令列工具

JackEggie發表於2019-04-14

用 Rust 打造你的第一個命令列工具

在精彩的程式設計世界裡,你可能聽說過這種名為 Rust 的新語言。它是一種開源的系統級程式語言。它專注於效能、記憶體安全和並行性。你可以像 C/C++ 那樣用它編寫底層應用程式。

你可能已經在 Web Assembly 網站上見到過它了。Rust 能夠編譯 WASM 應用程式,你可以在 Web Assembly FAQ 上找到很多例子。它也被認為是 servo 的基石,servo 是一個在 Firefox 中實現的高效能瀏覽器引擎。

這可能會讓你望而卻步,但這不是我們要在這裡討論的內容。我們將介紹如何使用它構建命令列工具,而你可能會從中發現很多有意思的東西。

為什麼是 Rust?

好吧,讓我把事情說清楚。我本可以用任何其他語言或框架來完成命令列工具。我可以選 C、Go、Ruby 等等。甚至,我可以使用經典的 bash。

在 2018 年中,我想學習一些新東西,Rust 激發了我的好奇心,同時我也需要構建一些簡單的小工具來自動化工作和個人專案中的一些流程。

安裝

你可以使用 Rustup 來設定你的開發環境,它是安裝和配置你機器上所有的 Rust 工具的主要入口。

如果你在 Linux 和 MacOS 上工作,使用如下命令即可完成安裝:

$ curl <https://sh.rustup.rs> -sSf | sh
複製程式碼

如果你使用的是 Windows 系統,同樣地,你需要在 Rustup 網站上下載一個 exe 並執行。

如果你用的是 Windows 10,我建議你使用 WSL 來完成安裝。以上就是安裝所需的步驟,我們現在可以去建立我們的第一個 Rust 應用程式了!

你的第一個 Rust 應用程式

我們在這裡要做的是,仿照 cat 來構建一個 UNIX 實用工具,或者至少是一個簡化版本,我們稱之為 kt。這個應用程式將接受一個檔案路徑作為輸入,並在終端的標準輸出中顯示檔案的內容。

要建立這個應用程式的基本框架,我們將使用一個名為 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 文件

這裡我們宣告的是我們想要使用 ArgApp 模組。我們希望我們的應用程式有一個 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~/.zshrcPATH 環境變數中)。

$ 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 ?

進一步探索

  • cat:cat 實用程式的 Wikipedia 頁面。
  • kt-rs
  • Rust Cookbook
  • Clap:一個功能齊全、高效能的 Rust 命令列引數解析器。
  • Reqwest:一個簡單而功能強大的 Rust HTTP 客戶端。
  • Serde:一個 Rust 的序列化框架。
  • crates.io: Rust 社群的工具註冊站點。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章