[譯]使用 Rust 開發一個簡單的 Web 應用,第 4 部分 —— CLI 選項解析

使用 Rust 開發一個簡單的 Web 應用,第 4 部分 —— CLI 選項解析

1 剛剛回到正軌

哈嘍!這兩天抱歉了哈。我和妻子剛買了房子,這兩天都在忙這個。感謝你的耐心等待。前端

2 簡介

在以前的文章中,咱們構建了一個「能跑起來」的應用;這證實了咱們的計劃可行。爲了使它真正用起來,咱們還須要關心好比說命令行選項之類的一些事情。android

因此,我要去作命令解析。但首先,咱們先將現存的代碼移出,以挪出空間咱們能夠作 CLI 解析實驗。但在此以前,咱們一般只須要移除舊文件,建立新 main.rsios

$ ls
Cargo.lock      Cargo.toml      log.txt         src             target
$ cd src/
$ ls
main.rs                 main_file_writing.rs    web_main.rs
複製代碼

main_file_writing.rsweb_main.rs 都是舊文件,因此我移除它們。而後我將 main.rs 重命名爲 main_logging_server.rs,而後建立新的 main.rsgit

$ 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 files
 2 files changed, 35 deletions(-)
 delete mode 100644 src/main_file_writing.rs
 delete 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 experiment
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename src/{main.rs => main_logging_server.rs} (100%)
$ touch main.rs
複製代碼

着眼於參數解析。在以前的帖子的評論部分,Stephan Sokolow 問我是否考慮過使用這個用於命令行解析的軟件包 clap。Clap 看起來頗有趣,因此我打算試試。程序員

3 需求

如下服務須要能被參數配置:github

  1. 日誌文件的位置。
  2. 用來進行身份驗證的私鑰。
  3. (可能)設置時間記錄使用的時區。

我剛剛查看了一下我打算用的 Digital Ocean 虛擬機,它是東部標準時間,也正是個人時區,因此我或許會暫時跳過第三條。web

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 run
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading ansi_term v0.6.3
 Downloading strsim v0.4.0
 Downloading clap v1.0.0-beta
   Compiling strsim v0.4.0
   Compiling ansi_term v0.6.3
   Compiling clap v1.0.0-beta
   Compiling 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 run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
     Running `target/debug/simple-log`
$ cargo run
     Running `target/debug/simple-log`
$ cargo build --release
   Compiling lazy_static v0.1.10
   Compiling matches v0.1.2
   Compiling bitflags v0.1.1
   Compiling httparse v0.1.2
   Compiling strsim v0.4.0
   Compiling rustc-serialize v0.3.14
   Compiling modifier v0.1.0
   Compiling libc v0.1.8
   Compiling unicase v0.1.0
   Compiling groupable v0.2.0
   Compiling regex v0.1.30
   Compiling traitobject v0.0.3
   Compiling pkg-config v0.3.4
   Compiling ansi_term v0.6.3
   Compiling gcc v0.3.5
   Compiling typeable v0.1.1
   Compiling unsafe-any v0.4.1
   Compiling num_cpus v0.2.5
   Compiling rand v0.3.8
   Compiling log v0.3.1
   Compiling typemap v0.3.2
   Compiling clap v1.0.0-beta
   Compiling plugin v0.2.6
   Compiling mime v0.0.11
   Compiling time v0.1.25
   Compiling openssl-sys v0.6.2
   Compiling openssl v0.6.2
   Compiling url v0.2.34
   Compiling mustache v0.6.1
   Compiling num v0.1.25
   Compiling cookie v0.1.20
   Compiling hyper v0.4.0
   Compiling chrono v0.2.14
   Compiling 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-beta

USAGE:
        simple-log [FLAGS]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

$ target/release/simple-log --help
simple-log v1.0-beta

USAGE:
        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 build
   Compiling gcc v0.3.5
   Compiling strsim v0.4.0
   Compiling typeable v0.1.1
   Compiling unicase v0.1.0
   Compiling ansi_term v0.6.3
   Compiling modifier v0.1.0
   Compiling httparse v0.1.2
   Compiling regex v0.1.30
   Compiling matches v0.1.2
   Compiling pkg-config v0.3.4
   Compiling lazy_static v0.1.10
   Compiling traitobject v0.0.3
   Compiling rustc-serialize v0.3.14
   Compiling libc v0.1.8
   Compiling groupable v0.2.0
   Compiling bitflags v0.1.1
   Compiling unsafe-any v0.4.1
   Compiling clap v1.0.0-beta
   Compiling typemap v0.3.2
   Compiling rand v0.3.8
   Compiling num_cpus v0.2.5
   Compiling log v0.3.1
   Compiling time v0.1.25
   Compiling openssl-sys v0.6.2
   Compiling plugin v0.2.6
   Compiling mime v0.0.11
   Compiling openssl v0.6.2
   Compiling url v0.2.34
   Compiling num v0.1.25
   Compiling mustache v0.6.1
   Compiling cookie v0.1.20
   Compiling hyper v0.4.0
   Compiling chrono v0.2.14
   Compiling 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-beta

USAGE:
        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 whodat
     Running `target/debug/simple-log --logfile whodat`
Logfile path: whodat
$ cargo run -- -l whodat
     Running `target/debug/simple-log -l whodat`
Logfile path: whodat
複製代碼

很棒,正常工做!但這有一個問題:

$ cargo run
     Running `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 occurred

To 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 run
   Compiling 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 occurred

To learn more, run the command again with --verbose.
$ cargo run -- -l whodat
     Running `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 whodat
   Compiling 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 whodat
   Compiling 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_pathauth_token 轉換成 String 類型。

在這我能確信的是,logfile_pathauth_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 build
   Compiling 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 whodat
   Compiling 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 whodat
   Compiling 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
    }
});
複製代碼

好吧,信息量很大。咱們來抽絲剝繭。

有兩個函數,restrictrestrict_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 build
   Compiling 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_pathauth_token 的全部權到調用的函數。這是問題的所在。

我未經訓練時,我認爲這是「正常」的,是代碼最天然的表現方式。我忽略了一個重要的警告:在當前狀況下,這個 lambda 表達式不能被調用一次以上。當它被第一次調用時,logfile_pathauth_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_pathauth_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 應用


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

相關文章
相關標籤/搜索