[译]使用 Rust 开发一个简单的 Web 应用,第 4 部分 —— CLI 选项解析
- 原文地址:A Simple Web App in Rust, Part 4 -- CLI Option Parsing
- 原文作者:Joel's Journal
- 譯文出自:掘金翻譯計劃
- 本文永久鏈接:github.com/xitu/gold-m…
- 譯者:LeopPro
使用 Rust 開發一個簡單的 Web 應用,第 4 部分 —— CLI 選項解析
1 剛剛回到正軌
哈嘍!這兩天抱歉了哈。我和妻子剛買了房子,這兩天都在忙這個。感謝你的耐心等待。
2 簡介
在之前的文章中,我們構建了一個“能跑起來”的應用;這證明了我們的計劃可行。為了使它真正用起來,我們還需要關心比如說命令行選項之類的一些事情。
所以,我要去做命令解析。但首先,我們先將現存的代碼移出,以挪出空間我們可以做 CLI 解析實驗。但在此之前,我們通常只需要移除舊文件,創建新 main.rs:
$ ls Cargo.lock Cargo.toml log.txt src target $ cd src/ $ ls main.rs main_file_writing.rs web_main.rs 復制代碼main_file_writing.rs 和 web_main.rs 都是舊文件,所以我移除它們。然后我將 main.rs 重命名為 main_logging_server.rs,然后創建新的 main.rs。
$ git rm main_file_writing.rs web_main.rs rm 'src/main_file_writing.rs' rm 'src/web_main.rs' $ git commit -m 'remove old files' [master 771380b] remove old files2 files changed, 35 deletions(-)delete mode 100644 src/main_file_writing.rsdelete mode 100644 src/web_main.rs $ git mv main.rs main_logging_server.rs $ git commit -m 'move main out of the way for cli parsing experiment' [master 4d24206] move main out of the way for cli parsing experiment1 file changed, 0 insertions(+), 0 deletions(-)rename src/{main.rs => main_logging_server.rs} (100%) $ touch main.rs 復制代碼著眼于參數解析。在之前的帖子的評論部分,Stephan Sokolow 問我是否考慮過使用這個用于命令行解析的軟件包 clap。Clap 看起來很有趣,所以我打算試試。
3 需求
以下服務需要能被參數配置:
我剛剛查看了一下我打算用的 Digital Ocean 虛擬機,它是東部標準時間,也正是我的時區,所以我或許會暫時跳過第三條。
4 實現
據我所知,設置 clap 依賴的方式是 clap = "*";。我更愿意指定一個具體的版本,但是現在“*”可以工作。
我新的 Cargo.toml 文件:
[package] name = "simple-log" version = "0.1.0" authors = ["Joel McCracken <mccracken.joel@gmail.com>"][dependencies]chrono = "0.2" clap = "*"[dependencies.nickel]git = "https://github.com/nickel-org/nickel.rs.git" 復制代碼安裝依賴:
$ cargo runUpdating registry `https://github.com/rust-lang/crates.io-index`Downloading ansi_term v0.6.3Downloading strsim v0.4.0Downloading clap v1.0.0-betaCompiling strsim v0.4.0Compiling ansi_term v0.6.3Compiling clap v1.0.0-betaCompiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) error: main function not found error: aborting due to previous error Could not compile `simple-log`.To learn more, run the command again with --verbose. 復制代碼這個錯誤只是因為我的 main.rs 還是空的;重要的是“編譯 clap”已經成功。
根據 README 文件,我會先嘗試一個非常簡單的版本:
extern crate clap; use clap::App;fn main() {let _ = App::new("fake").version("v1.0-beta").get_matches(); } 復制代碼運行:
$ cargo runCompiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)Running `target/debug/simple-log` $ cargo runRunning `target/debug/simple-log` $ cargo build --releaseCompiling lazy_static v0.1.10Compiling matches v0.1.2Compiling bitflags v0.1.1Compiling httparse v0.1.2Compiling strsim v0.4.0Compiling rustc-serialize v0.3.14Compiling modifier v0.1.0Compiling libc v0.1.8Compiling unicase v0.1.0Compiling groupable v0.2.0Compiling regex v0.1.30Compiling traitobject v0.0.3Compiling pkg-config v0.3.4Compiling ansi_term v0.6.3Compiling gcc v0.3.5Compiling typeable v0.1.1Compiling unsafe-any v0.4.1Compiling num_cpus v0.2.5Compiling rand v0.3.8Compiling log v0.3.1Compiling typemap v0.3.2Compiling clap v1.0.0-betaCompiling plugin v0.2.6Compiling mime v0.0.11Compiling time v0.1.25Compiling openssl-sys v0.6.2Compiling openssl v0.6.2Compiling url v0.2.34Compiling mustache v0.6.1Compiling num v0.1.25Compiling cookie v0.1.20Compiling hyper v0.4.0Compiling chrono v0.2.14Compiling nickel v0.5.0 (https://github.com/nickel-org/nickel.rs.git#69546f58)Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)$ target/debug/simple-log --help simple-log v1.0-betaUSAGE:simple-log [FLAGS]FLAGS:-h, --help Prints help information-V, --version Prints version information$ target/release/simple-log --help simple-log v1.0-betaUSAGE:simple-log [FLAGS]FLAGS:-h, --help Prints help information-V, --version Prints version information 復制代碼我不知道為什么自述文件告訴我要使用 --release 編譯 —— 似乎 debug 也一樣能工作。而我并不清楚將會發生什么。我們刪除掉 target 目錄,不加--release 再編譯一次:
$ rm -rf target $ ls Cargo.lock Cargo.toml log.txt src $ cargo buildCompiling gcc v0.3.5Compiling strsim v0.4.0Compiling typeable v0.1.1Compiling unicase v0.1.0Compiling ansi_term v0.6.3Compiling modifier v0.1.0Compiling httparse v0.1.2Compiling regex v0.1.30Compiling matches v0.1.2Compiling pkg-config v0.3.4Compiling lazy_static v0.1.10Compiling traitobject v0.0.3Compiling rustc-serialize v0.3.14Compiling libc v0.1.8Compiling groupable v0.2.0Compiling bitflags v0.1.1Compiling unsafe-any v0.4.1Compiling clap v1.0.0-betaCompiling typemap v0.3.2Compiling rand v0.3.8Compiling num_cpus v0.2.5Compiling log v0.3.1Compiling time v0.1.25Compiling openssl-sys v0.6.2Compiling plugin v0.2.6Compiling mime v0.0.11Compiling openssl v0.6.2Compiling url v0.2.34Compiling num v0.1.25Compiling mustache v0.6.1Compiling cookie v0.1.20Compiling hyper v0.4.0Compiling chrono v0.2.14Compiling nickel v0.5.0 (https://github.com/nickel-org/nickel.rs.git#69546f58)Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) $ target/release/simple-log --help bash: target/release/simple-log: No such file or directory $ target/debug/simple-log --help simple-log v1.0-betaUSAGE:simple-log [FLAGS]FLAGS:-h, --help Prints help information-V, --version Prints version information $ 復制代碼所以,我猜你并不需要加 --release。耶,每天學點新東西。
我們再回過頭來看 main 代碼,我注意到變量以 _ 命名;我們假定這是必須的,為了防止警告,表示廢棄。使用 _ 表示“故意未使用”真是漂亮的標準,我喜歡 Rust 對此支持。
好了,根據 clap 自述文件和上面的小實驗,我首次嘗試寫一個參數解析器:
extern crate clap; use clap::{App,Arg};fn main() {let matches = App::new("simple-log").version("v0.0.1").arg(Arg::with_name("LOG FILE").short("l").long("logfile").takes_value(true)).get_matches();println!("Logfile path: {}", matches.value_of("LOG FILE").unwrap());} 復制代碼=>
$ cargo run -- --logfile whodatRunning `target/debug/simple-log --logfile whodat` Logfile path: whodat $ cargo run -- -l whodatRunning `target/debug/simple-log -l whodat` Logfile path: whodat 復制代碼很棒,正常工作!但這有一個問題:
$ cargo runRunning `target/debug/simple-log` thread '<main>' panicked at 'called `Option::unwrap()` on a `None` value', /private/tmp/rust2015051 6-38954-h579wb/rustc-1.0.0/src/libcore/option.rs:362 An unknown error occurredTo learn more, run the command again with --verbose. 復制代碼看起來,在這調用 unwrap() 不是一個好主意,因為參數不一定被傳入!
我不清楚大型的 Rust 社區對 unwrap 的建議是什么,但我總能看見社區里提到為什么它應該可以在這里使用。然而我覺得這說得通,在應用規模增長的過程中,某位置失效是“喜聞樂見的”。錯誤發生在運行期。這不是編譯器可以檢測的出的!
unwrap 的基本思想是類似空指針異常么?我想是的。但是,它確實讓你停下來思考你在做什么,如果 unwrap 意味著代碼異味,這還不錯。這導致我有點想法想倒出來:
5 雜言
我堅信開發者的編碼質量不是語言層面能解決的問題。各類靜態語言社區總是花言巧語:“這些語言能使碼農遠離糟糕的編碼。”好啊,你猜怎么樣:這是不可能的。
首先,你沒法使用任何明確的方式定義“優秀的代碼”。確實,使代碼優秀的絕大多數原因是高內聚。舉一個非常簡單的例子,面條代碼在原型期往往是工作良好的,但在生產質量下,面條代碼是可怕的。
最近的 OpenSSL 漏洞就是最好的例證。在新聞中,我沒有得到多少信息,但我收集的資料表示,漏洞是由于錯誤的業務邏輯導致的。在某些極端情況下,攻擊者可以冒充 CA(可信第三方)。你如何通過編譯器預防此類問題呢?
確實,這將我帶回了 Charles Babbage 中的一個舊內容:
On two occasions I have been asked, "Pray, Mr. Babbage, if you put into the machine wrong figures, will the right answers come out?" In one case a member of the Upper, and in the other a member of the Lower, House put this question. I am not able rightly to apprehend the kind of confusion of ideas that could provoke such a question.
對此最好的辦法就是讓開發者更容易編程,讓正確的事情符合常規,容易達成。
當你認為靜態類型系統使編程更易的時候,我認為這件事又開始有意義了。說到底,開發者有責任保證程序行為正確,我們必須相信他們,賦予他們權利。
總而言之:程序員總是可以實現一個小的 Scheme 解釋器,并在其中編寫所有的應用程序邏輯。如果你試圖通過類型檢查器來防止這樣的事情,那么祝你好運咯。
好了,我說完了,我將放下我的話匣子。謝謝你容忍我喋喋不休。
6 繼續
回到主題上,我注意到有一個 Arg 的選項用來指定參數是否可選。我覺得我需要指定這個:
extern crate clap; use clap::{App,Arg};fn main() {let matches = App::new("simple-log").version("v0.0.1").arg(Arg::with_name("LOG FILE").short("l").long("logfile").required(true).takes_value(true)).get_matches();println!("Logfile path: {}", matches.value_of("LOG FILE").unwrap());} 復制代碼=>
$ cargo runCompiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)Running `target/debug/simple-log` error: The following required arguments were not supplied:'--logfile <LOG FILE>'USAGE:simple-log --logfile <LOG FILE>For more information try --help An unknown error occurredTo learn more, run the command again with --verbose. $ cargo run -- -l whodatRunning `target/debug/simple-log -l whodat` Logfile path: whodat 復制代碼奏效了!我們需要的下一個選項是通過命令行指定一個私鑰。讓我們添加它,但使其可選,因為,嗯,為什么不呢?我可能要搭建一個公開版本供人們預覽。
我這樣寫:
extern crate clap; use clap::{App,Arg};fn main() {let matches = App::new("simple-log").version("v0.0.1").arg(Arg::with_name("LOG FILE").short("l").long("logfile").required(true).takes_value(true)).arg(Arg::with_name("AUTH TOKEN").short("t").long("token").takes_value(true)).get_matches();let logfile_path = matches.value_of("LOG FILE").unwrap();let auth_token = matches.value_of("AUTH TOKEN"); } 復制代碼=>
$ cargo run -- -l whodatCompiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:17:9: 17:21 warning: unused variable: `logfile_path`, #[warn(unused_variables)] on by d efault src/main.rs:17 let logfile_path = matches.value_of("LOG FILE").unwrap();^~~~~~~~~~~~ src/main.rs:18:9: 18:19 warning: unused variable: `auth_token`, #[warn(unused_variables)] on by default src/main.rs:18 let auth_token = matches.value_of("AUTH TOKEN");^~~~~~~~~~Running `target/debug/simple-log -l whodat` 復制代碼這有很多(預料中的)警告,無妨,它成功編譯運行。我只是想檢查一下類型問題。現在讓我們真正開始編寫程序。我們以下面的代碼開始:
use std::io::prelude::*; use std::fs::OpenOptions; use std::io;#[macro_use] extern crate nickel; use nickel::Nickel;extern crate chrono; use chrono::{DateTime,Local};extern crate clap; use clap::{App,Arg};fn formatted_time_entry() -> String {let local: DateTime<Local> = Local::now();let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();formatted }fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {let mut file = try!(OpenOptions::new().append(true).write(true).create(true).open(filename));try!(file.write_all(bytes));Ok(()) }fn log_time(filename: &'static str) -> io::Result<String> {let entry = formatted_time_entry();{let bytes = entry.as_bytes();try!(record_entry_in_log(filename, &bytes));}Ok(entry) }fn do_log_time(logfile_path: &'static str, auth_token: Option<&str>) -> String {match log_time(logfile_path) {Ok(entry) => format!("Entry Logged: {}", entry),Err(e) => format!("Error: {}", e)} }fn main() {let matches = App::new("simple-log").version("v0.0.1").arg(Arg::with_name("LOG FILE").short("l").long("logfile").required(true).takes_value(true)).arg(Arg::with_name("AUTH TOKEN").short("t").long("token").takes_value(true)).get_matches();let logfile_path = matches.value_of("LOG FILE").unwrap();let auth_token = matches.value_of("AUTH TOKEN");let mut server = Nickel::new();server.utilize(router! {get "**" => |_req, _res| {do_log_time(logfile_path, auth_token)}});server.listen("127.0.0.1:6767"); } 復制代碼=>
$ cargo run -- -l whodatCompiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:60:24: 60:31 error: `matches` does not live long enough src/main.rs:60 let logfile_path = matches.value_of("LOG FILE").unwrap();^~~~~~~ note: reference must be valid for the static lifetime... src/main.rs:58:24: 72:2 note: ...but borrowed value is only valid for the block suffix following st atement 0 at 58:23 src/main.rs:58 .get_matches(); src/main.rs:59 src/main.rs:60 let logfile_path = matches.value_of("LOG FILE").unwrap(); src/main.rs:61 let auth_token = matches.value_of("AUTH TOKEN"); src/main.rs:62 src/main.rs:63 let mut server = Nickel::new();... src/main.rs:61:24: 61:31 error: `matches` does not live long enough src/main.rs:61 let auth_token = matches.value_of("AUTH TOKEN");^~~~~~~ note: reference must be valid for the static lifetime... src/main.rs:58:24: 72:2 note: ...but borrowed value is only valid for the block suffix following st atement 0 at 58:23 src/main.rs:58 .get_matches(); src/main.rs:59 src/main.rs:60 let logfile_path = matches.value_of("LOG FILE").unwrap(); src/main.rs:61 let auth_token = matches.value_of("AUTH TOKEN"); src/main.rs:62 src/main.rs:63 let mut server = Nickel::new();... error: aborting due to 2 previous errors Could not compile `simple-log`.To learn more, run the command again with --verbose. 復制代碼我不理解哪錯了 —— 這和例子實質上是一樣的。我嘗試注釋掉一堆代碼,直到它等效于下面的代碼:
fn main() {let matches = App::new("simple-log").version("v0.0.1").arg(Arg::with_name("LOG FILE").short("l").long("logfile").required(true).takes_value(true)).arg(Arg::with_name("AUTH TOKEN").short("t").long("token").takes_value(true)).get_matches();let logfile_path = matches.value_of("LOG FILE").unwrap();let auth_token = matches.value_of("AUTH TOKEN"); } 復制代碼…… 現在它可以編譯了。報了很多警告,但無妨。
上面的錯誤信息都不是被注釋掉的行產生的。現在我直到錯誤信息不一定指造成問題的代碼,我知道要去別處看看。
我做的第一件事是去掉對兩個參數的引用。代碼變成了這樣:
fn main() {let matches = App::new("simple-log").version("v0.0.1").arg(Arg::with_name("LOG FILE").short("l").long("logfile").required(true).takes_value(true)).arg(Arg::with_name("AUTH TOKEN").short("t").long("token").takes_value(true)).get_matches();let logfile_path = matches.value_of("LOG FILE").unwrap();let auth_token = matches.value_of("AUTH TOKEN");let mut server = Nickel::new();server.utilize(router! {get "**" => |_req, _res| {do_log_time("", Some(""))}});server.listen("127.0.0.1:6767"); } 復制代碼代碼成功的編譯運行。現在我了解了問題所在,我懷疑是GET請求被映射到 get ** 閉包中,而將這些變量傳入該閉包中引起了生命周期沖突。
我和我的朋友 Carol Nichols 討論了這個問題,她給我的建議使得我離解決問題更進一步:將 logfile_path 和 auth_token 轉換成 String 類型。
在這我能確信的是,logfile_path 和 auth_token 都是對于 matches 數據結構中某處的 str 類型的一個假借,它們在某一時間被傳出作用域。在 main 函數結尾?由于在閉包結束時 main 函數仍然在運行,似乎 matches 仍然存在。
另外,可能閉包不適用于假借變量。我覺得這似乎不太可能。似乎是編譯器無法肯定當閉包被調用時 matches 會仍然存在。即便如此,現在的情況仍然難以令人理解,因為閉包在 server 之中,將與 matches 同時結束作用域!
不管如何,我們這樣修改代碼:
// ... let logfile_path = matches.value_of("LOG FILE").unwrap(); let auth_token = matches.value_of("AUTH TOKEN");let mut server = Nickel::new(); server.utilize(router! {get "**" => |_req, _res| {do_log_time(logfile_path, auth_token)} }); // ... 復制代碼改成這樣:
// ... let logfile_path = matches.value_of("LOG FILE").unwrap().to_string(); let auth_token = match matches.value_of("AUTH TOKEN") {Some(str) => Some(str.to_string()),None => None };let mut server = Nickel::new(); server.utilize(router! {get "**" => |_req, _res| {do_log_time(logfile_path, auth_token)} });server.listen("127.0.0.1:6767"); // ... 復制代碼…… 解決了問題。我也令各個函數參數中的 &str 類型改為 String 類型。
當然,這揭示了一個新問題:
$ cargo buildCompiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:69:25: 69:37 error: cannot move out of captured outer variable in an `Fn` closure src/main.rs:69 do_log_time(logfile_path, auth_token)^~~~~~~~~~~~ <nickel macros>:1:1: 1:27 note: in expansion of as_block! <nickel macros>:10:12: 10:42 note: expansion site note: in expansion of closure expansion <nickel macros>:9:6: 10:54 note: expansion site <nickel macros>:1:1: 10:62 note: in expansion of _middleware_inner! <nickel macros>:4:1: 4:60 note: expansion site <nickel macros>:1:1: 7:46 note: in expansion of middleware! <nickel macros>:11:32: 11:78 note: expansion site <nickel macros>:1:1: 21:78 note: in expansion of _router_inner! <nickel macros>:4:1: 4:43 note: expansion site <nickel macros>:1:1: 4:47 note: in expansion of router! src/main.rs:67:20: 71:6 note: expansion site src/main.rs:69:39: 69:49 error: cannot move out of captured outer variable in an `Fn` closure src/main.rs:69 do_log_time(logfile_path, auth_token)^~~~~~~~~~ <nickel macros>:1:1: 1:27 note: in expansion of as_block! <nickel macros>:10:12: 10:42 note: expansion site note: in expansion of closure expansion <nickel macros>:9:6: 10:54 note: expansion site <nickel macros>:1:1: 10:62 note: in expansion of _middleware_inner! <nickel macros>:4:1: 4:60 note: expansion site <nickel macros>:1:1: 7:46 note: in expansion of middleware! <nickel macros>:11:32: 11:78 note: expansion site <nickel macros>:1:1: 21:78 note: in expansion of _router_inner! <nickel macros>:4:1: 4:43 note: expansion site <nickel macros>:1:1: 4:47 note: in expansion of router! src/main.rs:67:20: 71:6 note: expansion site error: aborting due to 2 previous errors Could not compile `simple-log`.To learn more, run the command again with --verbose. 復制代碼乍一看,我完全不能理解這個錯誤:
src/main.rs:69:25: 69:37 error: cannot move out of captured outer variable in an `Fn` closure src/main.rs:69 do_log_time(logfile_path, auth_token) 復制代碼它說的“移出”一個被捕獲的變量是什么意思?我不記得有哪個語言有這種移入、移出變量這樣的概念,那個錯誤信息對我來說難以理解。
錯誤信息也告訴了我一些其他奇怪的事情;什么是閉包必須擁有其中的對象?
我又上網查了查這個錯誤信息,有一些結果,但看起來沒有對我有用的。所以,我們接著玩耍。
7 更多的調試
首先,我先使用 --verbose 編譯看看能不能顯示一些有用的,但這并沒有打印任何關于此錯誤的額外信息,只是一些關于一般命令的。
我依稀記得 Rust 文檔中具體談到了閉包,所以我決定去看看。根據文檔,我猜測我需要一個“move”閉包。但當我嘗試的時候:
server.utilize(router! {get "**" => move |_req, _res| {do_log_time(logfile_path, auth_token)} }); 復制代碼…… 提示了一個新的錯誤信息:
$ cargo run -- -l whodatCompiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:66:21: 66:25 error: no rules expected the token `move` src/main.rs:66 get "**" => move |_req, _res| {^~~~ Could not compile `simple-log`.To learn more, run the command again with --verbose. 復制代碼這是我困惑,所以我決定試試把它移動到外面去:
foo = move |_req, _res| {do_log_time(logfile_path, auth_token) };server.utilize(router! {get "**" => foo }); 復制代碼=>
$ cargo run -- -l whodatCompiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:70:21: 70:24 error: no rules expected the token `foo` src/main.rs:70 get "**" => foo^~~ Could not compile `simple-log`.To learn more, run the command again with --verbose. 復制代碼出現了相同的錯誤信息。
這次我注意到,關于模式匹配宏系統的錯誤信息用詞看起來十分奇怪,我記得 router! 宏在這里被使用。一些宏很奇怪!我知道如何解決這個問題,因為我之前處理過。
$ rustc src/main.rs --pretty=expanded -Z unstable-options src/main.rs:5:14: 5:34 error: can't find crate for `nickel` src/main.rs:5 #[macro_use] extern crate nickel; 復制代碼據此,我猜,或許我需要給 cargo 傳遞這個參數So?查閱 cargo 文檔,沒有發現任何能傳遞參數給 rustc 的方式。
在網上搜索一波,我發現了一些 GitHub issues 提出傳遞任意參數是不被支持的,除非創建一個自定義 cargo 命令,這似乎從我現在要解決的問題轉移到了另一個可怕的問題,所以我不想接著這個思路走。
突然,一個瘋狂的想法浮現在我的腦海:當使用 cargo run --verbose時,我去看輸出中 rustc 命令是怎樣執行的:
# ... Caused by:Process didn't exit successfully: `rustc src/main.rs --crate-name simple_log --crate-type bin -g - -out-dir /Users/joel/Projects/simple-log/target/debug --emit=dep-info,link -L dependency=/Users/joel /Projects/simple-log/target/debug -L dependency=/Users/joel/Projects/simple-log/target/debug/deps -- extern nickel=/Users/joel/Projects/simple-log/target/debug/deps/libnickel-0a4cb77ee6c08a8b.rlib --ex tern chrono=/Users/joel/Projects/simple-log/target/debug/deps/libchrono-a9b06d7e3a59ae0d.rlib --exte rn clap=/Users/joel/Projects/simple-log/target/debug/deps/libclap-01156bdabdb6927f.rlib -L native=/U sers/joel/Projects/simple-log/target/debug/build/openssl-sys-9c1a0f13b3d0a12d/out -L native=/Users/j oel/Projects/simple-log/target/debug/build/time-30c208bd835b525d/out` (exit code: 101) # ... 復制代碼…… 我這個騷操作:我能否修改 rustc 的編譯指令,輸出宏擴展代碼呢?我們試一下:
$ rustc src/main.rs --crate-name simple_log --crate-type bin -g --out-dir /Users/joel/Projects/simple-log/target/debug --emit=dep-info,link -L dependency=/Users/joel/Projects/simple-log/target/debug -L dependency=/Users/joel/Projects/simple-log/target/debug/deps --extern nickel=/Users/joel/Projects/simple-log/target/debug/deps/libnickel-0a4cb77ee6c08a8b.rlib --extern chrono=/Users/joel/Projects/simple -log/target/debug/deps/libchrono-a9b06d7e3a59ae0d.rlib --extern clap=/Users/joel/Projects/simple-log/target/debug/deps/libclap-01156bdabdb6927f.rlib -L native=/Users/joel/Projects/simple-log/target/debu g/build/openssl-sys-9c1a0f13b3d0a12d/out -L native=/Users/joel/Projects/simple-log/target/debug/build/time-30c208bd835b525d/out --pretty=expanded -Z unstable-options > macro-expanded.rs $ cat macro-expanded.rs #![feature(no_std)] #![no_std] #[prelude_import] use std::prelude::v1::*; #[macro_use] extern crate std as std; use std::io::prelude::*; ... 復制代碼它奏效了!這種操作登不得大雅之堂,但有時就是偏方才奏效,我至少弄明白了。這也讓我弄清了 cargo 是怎樣調用 rustc 的。
對我們有用的輸出部分是這樣的:
server.utilize({use nickel::HttpRouter;let mut router = ::nickel::Router::new();{router.get("**",{use nickel::{MiddlewareResult, Responder, Response, Request};#[inline(always)]fn restrict<'a, R: Responder>(r: R, res: Response<'a>) -> MiddlewareResult<'a> {res.send(r)}#[inline(always)]fn restrict_closure<F>(f: F) -> F where F: for<'r, 'b, 'a>Fn(&'r mut Request<'b, 'a, 'b>, Response<'a>) -> MiddlewareResult<'a> + Send + Sync {f}restrict_closure(move |_req, _res| { restrict({ do_log_time(logfile_path, auth_token)}, _res)})});router} }); 復制代碼好吧,信息量很大。我們來抽絲剝繭。
有兩個函數,restrict 和 restrict_closure,這令我驚訝。我認為它們的存在是為了提供更好的關于這些請求處理閉包的類型 / 錯誤信息。
然而,這還有許多有趣的事情:
restrict_closure(move |_req, _res| { ... }) 復制代碼…… 這告訴我,宏指定了閉包是 move 閉包。從理論上,是這樣的。
8 重構
我們重構,并且重新審視一下這個問題。這一次,main 函數是這樣的:
fn main() {let matches = App::new("simple-log").version("v0.0.1").arg(Arg::with_name("LOG FILE").short("l").long("logfile").required(true).takes_value(true)).arg(Arg::with_name("AUTH TOKEN").short("t").long("token").takes_value(true)).get_matches();let logfile_path = matches.value_of("LOG FILE").unwrap().to_string();let auth_token = match matches.value_of("AUTH TOKEN") {Some(str) => Some(str.to_string()),None => None};let mut server = Nickel::new();server.utilize(router! {get "**" => |_req, _res| {do_log_time(logfile_path, auth_token)}});server.listen("127.0.0.1:6767"); } 復制代碼編譯時輸出為:
$ cargo buildCompiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log) src/main.rs:69:25: 69:37 error: cannot move out of captured outer variable in an `Fn` closure src/main.rs:69 do_log_time(logfile_path, auth_token)^~~~~~~~~~~~ <nickel macros>:1:1: 1:27 note: in expansion of as_block! <nickel macros>:10:12: 10:42 note: expansion site note: in expansion of closure expansion <nickel macros>:9:6: 10:54 note: expansion site <nickel macros>:1:1: 10:62 note: in expansion of _middleware_inner! <nickel macros>:4:1: 4:60 note: expansion site <nickel macros>:1:1: 7:46 note: in expansion of middleware! <nickel macros>:11:32: 11:78 note: expansion site <nickel macros>:1:1: 21:78 note: in expansion of _router_inner! <nickel macros>:4:1: 4:43 note: expansion site <nickel macros>:1:1: 4:47 note: in expansion of router! src/main.rs:67:20: 71:6 note: expansion site src/main.rs:69:39: 69:49 error: cannot move out of captured outer variable in an `Fn` closure src/main.rs:69 do_log_time(logfile_path, auth_token)^~~~~~~~~~ <nickel macros>:1:1: 1:27 note: in expansion of as_block! <nickel macros>:10:12: 10:42 note: expansion site note: in expansion of closure expansion <nickel macros>:9:6: 10:54 note: expansion site <nickel macros>:1:1: 10:62 note: in expansion of _middleware_inner! <nickel macros>:4:1: 4:60 note: expansion site <nickel macros>:1:1: 7:46 note: in expansion of middleware! <nickel macros>:11:32: 11:78 note: expansion site <nickel macros>:1:1: 21:78 note: in expansion of _router_inner! <nickel macros>:4:1: 4:43 note: expansion site <nickel macros>:1:1: 4:47 note: in expansion of router! src/main.rs:67:20: 71:6 note: expansion site error: aborting due to 2 previous errors Could not compile `simple-log`.To learn more, run the command again with --verbose. 復制代碼我在 IRC(一種即時通訊系統) 中問了這個問題,但是沒有得到回應。按道理講,我應該多花費一些耐心在 IRC 上提問,但沒有就是沒有。
我在 nickel.rs 項目上提交了一個 Issue,認為該問題是由宏導致的。這是我最終的想法 —— 我知道我可能是錯的,但是我沒有看到別的方法,我也不想放棄。
我的 Issue 在 github.com/nickel-org/…。Ryman 很快看到了我的錯誤,并且非常友好的幫助我解決了問題。顯然,他是對的 —— 如果你能看到這篇文章,Ryman,我欠你一個人情。
問題發生在以下具體的閉包中。我們檢查一下看看我們能發現什么:
get "**" => |_req, _res| {do_log_time(logfile_path, auth_token) } 復制代碼你注意到沒,這里,對 do_log_time 的調用轉移了 logfile_path 和 auth_token 的所有權到調用的函數。這是問題的所在。
我未經訓練時,我認為這是“正常”的,是代碼最自然的表現方式。我忽略了一個重要的警告:在當前情況下,這個 lambda 表達式不能被調用一次以上。當它被第一次調用時,logfile_path 和 auth_token 的所有權被轉移到了 do_log_time 的調用者。這就是說:如果這個函數再次被調用,它不能再轉移所有權給 do_log_time,因為它不再擁有這兩個變量。
因此,我們得到錯誤信息:
src/main.rs:69:39: 69:49 error: cannot move out of captured outer variable in an `Fn` closure 復制代碼我仍然認為這沒有任何意義,但是現在至少我明白,它是將所有權從閉包中“移出”。
無論如何,解決這個問題最簡單的方法是這樣:
let mut server = Nickel::new(); server.utilize(router! {get "**" => |_req, _res| {do_log_time(logfile_path.clone(), auth_token.clone())} }); 復制代碼現在,在每次調用中,logfile_path 和 auth_token 仍然被擁有,克隆體被創建了,其所有權被轉移了。
然而,我想指出,我仍然認為這是一個次優的解決方案。因為轉移所有權的過程不夠透明,我現在傾向于盡可能使用引用。
如果使用顯式的符號來代表假借的引用用另一種顯式符號代表擁有,Rust 會更好,* 起這個作用嗎?我不知道,但是這的確是一個有趣的問題。
9 重構
我將嘗試一個快速重構,看看我是否可以使用引用。這將是有趣的,因為我可能會出現一些不可預見的問題 —— 我們來看看吧!
我一直在閱讀 Martin Fowler 寫的關于重構的書,這刷新了我的價值觀,做事情要從一小步開始。第一步,我只想將所有權轉化為假借;我們從 logfile_path 開始:
fn do_log_time(logfile_path: String, auth_token: Option<String>) -> String {match log_time(logfile_path) {Ok(entry) => format!("Entry Logged: {}", entry),Err(e) => format!("Error: {}", e)} }// ...fn main() {// ...server.utilize(router! {get "**" => |_req, _res| {do_log_time(logfile_path.clone(), auth_token.clone())}});// ... } 復制代碼改為:
fn do_log_time(logfile_path: &String, auth_token: Option<String>) -> String {match log_time(logfile_path.clone()) {Ok(entry) => format!("Entry Logged: {}", entry),Err(e) => format!("Error: {}", e)} }// ...fn main() {// ...server.utilize(router! {get "**" => |_req, _res| {do_log_time(&logfile_path, auth_token.clone())}});// ... } 復制代碼這次重構一定要實現:用假借替代所有權和克隆。如果我擁有一個對象,并且我要將其轉化為假借,而且我還想在其他地方轉移其所有權,我必須先在內部創建自己的副本。這使我可以將我的所有權變成假借,在必要的時候我仍然可以轉移所有權。當然,這涉及克隆假借的對象,這會重復占用內存以及產生性能開銷,但如此一來我可以安全地更改這行代碼。然后,我可以持續使用假借取代所有權,而不會破壞任何東西。
嘗試了多次之后我得到如下代碼:
use std::io::prelude::*; use std::fs::OpenOptions; use std::io;#[macro_use] extern crate nickel; use nickel::Nickel;extern crate chrono; use chrono::{DateTime,Local};extern crate clap; use clap::{App,Arg};fn formatted_time_entry() -> String {let local: DateTime<Local> = Local::now();let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();formatted }fn record_entry_in_log(filename: &String, bytes: &[u8]) -> io::Result<()> {let mut file = try!(OpenOptions::new().append(true).write(true).create(true).open(filename));try!(file.write_all(bytes));Ok(()) }fn log_time(filename: &String) -> io::Result<String> {let entry = formatted_time_entry();{let bytes = entry.as_bytes();try!(record_entry_in_log(filename, &bytes));}Ok(entry) }fn do_log_time(logfile_path: &String, auth_token: &Option<String>) -> String {match log_time(logfile_path) {Ok(entry) => format!("Entry Logged: {}", entry),Err(e) => format!("Error: {}", e)} }fn main() {let matches = App::new("simple-log").version("v0.0.1").arg(Arg::with_name("LOG FILE").short("l").long("logfile").required(true).takes_value(true)).arg(Arg::with_name("AUTH TOKEN").short("t").long("token").takes_value(true)).get_matches();let logfile_path = matches.value_of("LOG FILE").unwrap().to_string();let auth_token = match matches.value_of("AUTH TOKEN") {Some(str) => Some(str.to_string()),None => None};let mut server = Nickel::new();server.utilize(router! {get "**" => |_req, _res| {do_log_time(&logfile_path, &auth_token)}});server.listen("127.0.0.1:6767");} 復制代碼我馬上需要處理 auth_token,但現在應該暫告一段落。
10 對第四部分的結論與回顧
應用程序現在具有解析選項的功能了。然而,這是非常困難的。在嘗試解決我的問題時,我差點走投無路。如果我在 nickel.rs 提出的 Issue 沒有這么有幫助的回應的話,我會非常受挫。
一些教訓:
- 轉讓所有權是一件棘手的事情。我認為對我來說,一個新的經驗之談是,如果不必使用所有權,盡量通過不可變的假借來傳遞參數。
- Cargo 真應該提供一個直接傳參給 rustc 的方法。
- 一些 Rust 錯誤提示不那么太好。
- 即使錯誤信息很不怎么好,Rust 還是對的 —— 向我的閉包中轉移所有權是錯誤的,因為網頁每被請求一次,該函數就被調用一次。這里給我的一個教訓是:如果我不明白錯誤信息,那么以代碼為切入點來思考問題是個好辦法,尤其是思考什么與 Rust 保證內存安全的思想相左。
這個經驗也加強了我對強類型程序語言編譯失敗的承受能力。有時,你真的要去了解內部發生的事情以清楚正在發生什么。在本例中,很難去創建一個最小可重現錯誤來說明問題。
當錯誤消息沒有給你你需要的信息時,你下一步最好的選擇是開始在互聯網上搜索與錯誤消息相關的信息。這并不能真正幫助你自己調查,理解和解決問題。
我認為這可以通過增加一些在多次不同狀態下詢問編譯器結果來優化,以找到關于該問題的更多信息。就像在編譯錯誤中打開一個交互式提示一樣,這真是太好了,但即使是注釋代碼以從編譯器請求詳細信息也是非常有用的。
—
我在大約一個月的時間里寫了這篇文章,主要是因為我忙于處理房子購置物品。有時候,我對此感到非常沮喪。我以為整合選項解析是最簡單的任務!
但是,意識到 Rust 揭示了我程序的問題時,緩解了我的心情。即使錯誤信息不如我所希望的那樣好,我還是喜歡它能合理的分割錯誤,這使我從中被拯救出來。
我希望隨著Rust的成熟,錯誤信息會變得更好。如隨我愿,我想我所有的擔心都會消失。
—
系列文章:使用 Rust 開發一個簡單的 Web 應用
- 第 1 部分
- 第 2a 部分
- 第 2b 部分
- 第 3 部分
- 第 4 部分
- 總結
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、后端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。
總結
以上是生活随笔為你收集整理的[译]使用 Rust 开发一个简单的 Web 应用,第 4 部分 —— CLI 选项解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: component lists rend
- 下一篇: 网络工程师成长日记365-IBIS西安工