- A Simple Web App in Rust, Part 4 — CLI Option Parsing
- Joel’s Journal
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: LeopPro
Developing a simple Web application with Rust, Part 4 — CLI options parsing
1 is just getting back on track
Hello! Sorry about the last two days. My wife and I just bought a house and we’ve been working on this for two days. Thank you for your patience.
2 brief introduction
In the previous article, we built a “running” application; This proves that our plan is feasible. To make it really work, we need to care about things like command-line options.
So, I’m going to do command parsing. But first, let’s move the existing code out to make room for CLI parsing experiments. But before that, we usually just remove the old file and create a new main.rs:
$ ls
Cargo.lock Cargo.toml log.txt src target
$ cd src/
$ ls
main.rs main_file_writing.rs web_main.rs
Copy the code
Main_file_writing. rs and web_main.rs are old files, so I remove them. I then rename main.rs to main_logging_server.rs and create a new 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 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
Copy the code
Look at parameter parsing. In the comments section of a previous post, Stephan Sokolow asked me if I’d considered Clap, a software package for command line parsing. The Clap looks fun, so I’m going to give it a try.
Three requirements
The following services need to be configurable:
- Location of the log file.
- The private key used for authentication.
- (possibly) set the time zone to use for the time record.
I just checked on the Digital Ocean VIRTUAL machine I’m planning to use, and it’s Eastern Standard Time, which is my time zone, so I’ll probably skip item 3 for now.
4 implement
As far as I know, the way to set a clap dependency is clap = “*”; . I prefer to specify a specific version, but for now “*” works.
My new Cargo. Toml file:
[package]
name = "simple-log"
version = "0.1.0 from"
authors = ["Joel McCracken <[email protected]>"]
[dependencies]
chrono = "0.2"
clap = "*"
[dependencies.nickel]
git = "https://github.com/nickel-org/nickel.rs.git"
Copy the code
Install dependencies:
$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 The Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) error: mainfunction not found
error: aborting due to previous error
Could not compile `simple-log`.
To learn more, run the command again with --verbose.
Copy the code
This error is simply because my main.rs is still empty; The important thing is that the “compile Clap” has succeeded.
Based on the README file, I’ll try a very simple version first:
extern crate clap;
use clap::App;
fn main() {
let _ = App::new("fake").version("V1.0 - beta").get_matches();
}
Copy the code
Run:
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) Running ` target/debug/simple - log ` $ Cargo run Running 'target/debug/ simle-log' $cargo build --release 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 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 un-any v0.4.1 Compiling num_cpus v0.2.5 Compiling rand v0.3.8 CompilinglogV0.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 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)The 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 PrintshelpInformation -v, --version Prints version information $target/release/simple-log --help simple-log v1.0-beta USAGE: simple-log [FLAGS] FLAGS: -h, --help Printshelp information
-V, --version Prints version information
Copy the code
I don’t know why the readme tells me to use –release to compile — debug seems to work just as well. I don’t know what’s going to happen. We will delete the target directory without –release and compile again:
$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 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 un-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 CompilinglogV0.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 The hyper v0.4.0 Compiling chrono v0.2.14 Compiling nickel v0.5.0 (https://github.com/nickel-org/nickel.rs.git#69546f58)The 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 Printshelp information
-V, --version Prints version information
$
Copy the code
So, I guess you don’t need –release. Yeah, learn something new every day.
Going back to the main code, I notice that variables are named _; We assume that this is necessary in order to prevent warnings indicating deprecation. Using _ for “intentionally unused” is a nice standard, and I like that Rust supports it.
Well, based on the Clap readme file and the little experiment above, I’m trying to write a parameter parser for the first time:
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());
}
Copy the code
= >
$ 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
Copy the code
Great, working! But there’s a problem:
$ 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. The rs: 362, An unknown error occurred To learn more. run thecommand again with --verbose.
Copy the code
It seems that calling unwrap() here is not a good idea, since arguments are not necessarily passed in!
I don’t know what the larger Rust community is suggesting to Unwrap, but I see it mentioned all the time as to why it should work here. It makes sense to me, however, that as applications grow, location failures are “welcome”. The error occurs at run time. This is not something the compiler can detect!
Is the basic idea of unwrap like a null-pointer exception? I think so. However, it does make you stop and think about what you’re doing, which is fine if unwrap means code stinks. This leads me to some thoughts about pouring it out:
5 miscellaneous word
I firmly believe that the quality of developer coding is not a problem that can be solved at the language level. Static language communities of all kinds have a lot of rhetoric: “These languages keep code farmers away from bad coding.” Well, guess what: it’s not going to happen.
First, you can’t define “good code” in any specific way. Indeed, most of what makes code great is high cohesion. To take a very simple example, noodle code tends to work well in the prototype phase, but at production quality, noodle code is terrible.
The recent OpenSSL breach is a case in point. I didn’t get much information in the news, but what I did gather suggested that the bug was caused by faulty business logic. In some extreme cases, an attacker can impersonate a CA (trusted third party). How do you prevent such problems with the compiler?
Indeed, this brings me back to an old part of 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.
The best way to do that is to make it easier for developers to program, to make the right things routine and easy to do.
When you think that static type systems make programming easier, I think it makes sense again. At the end of the day, developers are responsible for making sure their programs behave correctly, and we have to trust them and empower them.
To sum up: Programmers can always implement a small Scheme interpreter and write all the application logic in it. If you’re trying to prevent this from happening with a type checker, good luck.
Okay, I’m done. I’m going to put down my chatterbox. Thank you for tolerating my chatter.
6 to continue
Returning to the subject, I notice that there is an Arg option that specifies whether the parameter is optional. I think I need to specify this:
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());
}
Copy the code
= >
$cargo run the 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
Copy the code
Successful! The next option we need is to specify a private key from the command line. Let’s add it, but make it optional, because, well, why not? I might have to build a public version for people to preview.
I’ll write it like this:
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");
}
Copy the code
= >
$ cargo run -- -lWhodat Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) SRC/main. Rs: 17:9: time 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`
Copy the code
This has a lot of (expected) caveats, but it compiles and runs successfully. I just want to check the type. Now let’s actually start writing the program. Let’s start with the following code:
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<()> {
letmut 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
{ 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");
}
Copy the code
= >
$ cargo run -- -lWhodat 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:60let logfile_path = matches.value_of("LOG FILE").unwrap();
^~~~~~~
note: reference must be valid forthe static lifetime... src/main.rs:58:24: 72:2 note: ... but borrowed value is only validfor 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 letmut server = Nickel::new(); . src/main.rs:61:24: 61:31 error: `matches` does not live long enough src/main.rs:61let auth_token = matches.value_of("AUTH TOKEN");
^~~~~~~
note: reference must be valid forthe static lifetime... src/main.rs:58:24: 72:2 note: ... but borrowed value is only validfor 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 letmut server = Nickel::new(); . error: aborting due to 2 previous errors Could not compile `simple-log`. To learn more, run thecommand again with --verbose.
Copy the code
I don’t understand what’s wrong — this is essentially the same as the example. I tried to comment out a bunch of code until it was equivalent to the following code:
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");
}
Copy the code
… Now it’s ready to compile. Plenty of warnings, but no harm.
None of the above error messages were generated by commented out lines. Now THAT I know the error message doesn’t necessarily refer to the code causing the problem, I know to look elsewhere.
The first thing I did was remove references to two parameters. The code looks like this:
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");
}
Copy the code
The code compiled and ran successfully. Now that I understand the problem, I suspect that the GET request is mapped to the GET ** closure, and passing these variables into that closure causes a lifecycle conflict.
I discussed this with my friend Carol Nichols, and she gave me a suggestion that brought me one step closer to solving the problem: convert logFile_PATH and auth_token to strings.
All I know for sure is that logFILe_PATH and auth_token are both a pseudonym for the STR type somewhere in the matches data structure, and they were passed out of scope at some point. At the end of main? Since main is still running when the closure ends, it looks like matches is still there.
In addition, perhaps closures do not apply to bogus variables. It seems unlikely to me. It seems that the compiler cannot be sure matches will still exist when the closure is called. Even so, the situation is still hard to understand because the closure is in Server and ends scope at the same time as matches!
Anyway, we modify the code like this:
// ...
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)
}
});
// ...
Copy the code
Change it to this:
// ...
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");
// ...
Copy the code
… Solved the problem. I also changed the &str type in each function argument to String.
Of course, this reveals a new problem:
$cargo build the 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 variablein 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.
Copy the code
At first glance, I couldn’t understand this mistake at all:
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)
Copy the code
What does it mean to “remove” a captured variable? I can’t remember any language that has such a concept of moving variables in and out, and the error message was incomprehensible to me.
The error message also told me some other strange things; What is a closure that must own its objects?
I went online to look up the error message again, and there were some results, but none seemed to be useful to me. So we went on playing.
7 More debugging
First, I use –verbose compilation to see if I can show anything useful, but this doesn’t print any additional information about the error, just general commands.
I vaguely remembered the Rust documentation specifically talking about closures, so I decided to take a look. Based on the documentation, I guess I need a “move” closure. But when I tried:
server.utilize(router! {
get "* *" => move |_req, _res| {
do_log_time(logfile_path, auth_token)
}
});
Copy the code
… A new error message is displayed:
$ cargo run -- -lWhodat Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) SRC/main. Rs: 66:21:66: error: 25 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.
Copy the code
This confused me, so I decided to try moving it outside:
foo = move |_req, _res| {
do_log_time(logfile_path, auth_token)
};
server.utilize(router! {
get "* *" => foo
});
Copy the code
= >
$ cargo run -- -lWhodat Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) SRC/main. Rs: 70:21:70: 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.
Copy the code
The same error message appears.
This time I noticed that the words in the error message about the pattern-matching macro system seemed very strange. I remembered router! Macros are used here. Some macros are weird! I know how to solve this problem because I’ve dealt with it before.
$ 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;
Copy the code
So, I guess, maybe I need to pass cargo the parameter So? Looking at the CARGO documentation, I didn’t find any way to pass parameters to THE RUSTC.
Searching around the web, I came across some GitHub issues that say passing arbitrary parameters is not supported unless you create a custom cargo command, which seems to have shifted from the problem I’m trying to solve to another scary one, so I don’t want to go down that path.
Suddenly, a crazy thought occurred to me: when using cargo Run –verbose, I went to see how the rustc command was executed in the output:
#...
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)
# ...
Copy the code
… I this SAO operation: can I modify ruSTC compile instructions, output macro extension code? Let’s try:
$ 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::*; .Copy the code
It worked! It’s undignified, but sometimes it’s the folk remedy that works, and AT least I figured it out. This also helped me understand how Cargo called RUSTC.
The output that is useful to us looks like this:
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 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
}
});
Copy the code
Well, that’s a lot of information. Let’s get rid of it.
There are two functions, RESTRICT and restrict_closure, which surprised me. I think they exist to provide better type/error information about these request processing closures.
However, there are a number of interesting things:
restrict_closure(move |_req, _res| { ... })
Copy the code
… This tells me that the macro specifies that the closure is a move closure. In theory, yes.
8 refactoring
Let’s refactor and revisit the problem. This time, main looks like this:
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");
}
Copy the code
Compile time output is:
$cargo build the 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 variablein 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.
Copy the code
I asked this question in IRC (an instant messaging system) and got no response. Logically, I should have spent more patience asking questions on IRC, but no means no.
I submitted an Issue on the Nickel.rs project thinking that the problem was caused by macros. That was my final thought — I knew I could be wrong, but I didn’t see another way, and I didn’t want to give up.
My Issue is at github.com/nickel-org/… . Ryman quickly saw my mistake and was kind enough to help me fix it. Apparently, he’s right — if you can read this, Ryman, I owe you one.
The problem occurs in the following specific closures. Let’s check and see what we can find:
get "* *" => |_req, _res| {
do_log_time(logfile_path, auth_token)
}
Copy the code
Notice here that the call to do_log_time transfers ownership of logfile_path and auth_token to the called function. That’s the problem.
When I was untrained, I thought this was “normal,” the most natural way code should behave. I omitted one important caveat: in the current case, the lambda expression cannot be called more than once. When it is first called, the ownership of logFILe_path and auth_token is transferred to the caller of do_log_time. That is: if this function is called again, it can no longer transfer ownership to do_log_time because it no longer owns these two variables.
Therefore, we get the error message:
src/main.rs:69:39: 69:49 error: cannot move out of captured outer variable in an `Fn` closure
Copy the code
I still don’t think this makes any sense, but now at least I understand that it “moves” ownership out of the closure.
Anyway, the easiest way to solve the problem is this:
let mut server = Nickel::new();
server.utilize(router! {
get "* *" => |_req, _res| {
do_log_time(logfile_path.clone(), auth_token.clone())
}
});
Copy the code
Now, on each call, logFile_PATH and Auth_token are still owned, the clone is created, and its ownership is transferred.
However, I would like to point out that I still think this is a suboptimal solution. Because the process of transferring ownership is not transparent, I now prefer to use references whenever possible.
If Rust is better off using an explicit symbol for a spurious reference and another explicit symbol for possession, * does it work? I don’t know, but it’s an interesting question.
9 refactoring
I’ll try a quick refactoring to see if I can use references. This will be interesting because I may have some unforeseen problems – let’s see!
I’ve been reading books about refactoring by Martin Fowler, and it’s refreshed my value system to start with one small step. The first step, I just want to convert the ownership into a loan; We’ll start with 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())
}
});
// ...
}
Copy the code
To:
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())
}
});
// ...
}
Copy the code
This refactoring must be done: replace ownership and cloning with pretence. If I own an object and I want to convert it to a fake, and I want to transfer ownership elsewhere, I must first create my own copy internally. This allows me to turn my ownership into a sham, and I can still transfer ownership if necessary. Of course, this involves cloning the fake object, which can duplicate memory and incur performance overhead, but this way I can safely change this line of code. Then, I can continue to use pretence in lieu of ownership without destroying anything.
After several attempts I get the following code:
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<()> {
letmut 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();
{
letbytes = 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");
}
Copy the code
I need to deal with auth_token right away, but I should leave it at that for now.
Conclusion and review of part IV
The application now has the ability to parse options. However, this is very difficult. I almost had my back to the wall trying to solve my problem. I would have been frustrated if MY Issue at Nickel.RS had not received such a helpful response.
Some lessons:
- Transferring ownership is a tricky business. I think a new rule of thumb for me is, if you don’t have to use ownership, try to pass parameters through immutable pseudonyms.
- Cargo Really shouldProvide a direct reference to
rustc
Methods. - Some Rust errors are not so good.
- Even if the error message is bad, Rust is right — it’s wrong to transfer ownership to my closure because this function is called every time a web page is requested. The lesson here is that if I don’t understand error messages, it’s a good idea to think in terms of code, especially what is at odds with Rust’s idea of memory security.
This experience also strengthened my tolerance for compiler failures in strongly typed programming languages. Sometimes, you really have to get inside what’s going on to understand what’s going on. In this case, it is difficult to create a minimum reproducible error to illustrate the problem.
When an error message doesn’t give you the information you need, your best next step is to start searching the Internet for information related to the error message. It doesn’t really help you investigate, understand and solve problems on your own.
I think this can be optimized by adding something to ask the compiler for results in multiple different states to find out more about the problem. This is all very well and good, like turning on an interactive prompt for a compilation error, but even annotating the code to request details from the compiler is useful.
–
I wrote this article in about a month, mainly because I was busy with house purchases. Sometimes, I feel very depressed about it. I thought integrating option resolution was the easiest task!
But it was a relief to realize that Rust had revealed problems with my program. Even if the error message isn’t as good as I’d like it to be, I like the fact that it splits the error properly and saves me from it.
I hope that error messages get better as Rust matures. If I had my way, I think all my worries would go away.
–
Article series: Developing a Simple Web application using Rust
- Part 1
- The second part a
- The second part b
- Part 3
- Part 4
- conclusion
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.