- 原文地址:Your first CLI tool with Rust
- 原文做者:Jérémie Veillet
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:JackEggie
- 校对者:TloveYing
在精彩的编程世界里,你可能据说过这种名为 Rust 的新语言。它是一种开源的系统级编程语言。它专一于性能、内存安全和并行性。你能够像 C/C++ 那样用它编写底层应用程序。html
你可能已经在 Web Assembly 网站上见到过它了。Rust 可以编译 WASM 应用程序,你能够在 Web Assembly FAQ 上找到不少例子。它也被认为是 servo 的基石,servo 是一个在 Firefox 中实现的高性能浏览器引擎。前端
这可能会让你望而却步,但这不是咱们要在这里讨论的内容。咱们将介绍如何使用它构建命令行工具,而你可能会从中发现不少有意思的东西。android
好吧,让我把事情说清楚。我本能够用任何其余语言或框架来完成命令行工具。我能够选 C、Go、Ruby 等等。甚至,我可使用经典的 bash。ios
在 2018 年中,我想学习一些新东西,Rust 激发了个人好奇心,同时我也须要构建一些简单的小工具来自动化工做和我的项目中的一些流程。git
你可使用 Rustup 来设置你的开发环境,它是安装和配置你机器上全部的 Rust 工具的主要入口。github
若是你在 Linux 和 MacOS 上工做,使用以下命令便可完成安装:web
$ curl <https://sh.rustup.rs> -sSf | sh
复制代码
若是你使用的是 Windows 系统,一样地,你须要在 Rustup 网站上下载一个 exe
并运行。编程
若是你用的是 Windows 10,我建议你使用 WSL 来完成安装。以上就是安装所需的步骤,咱们如今能够去建立咱们的第一个 Rust 应用程序了!json
咱们在这里要作的是,仿照 cat 来构建一个 UNIX 实用工具,或者至少是一个简化版本,咱们称之为 kt
。这个应用程序将接受一个文件路径做为输入,并在终端的标准输出中显示文件的内容。windows
要建立这个应用程序的基本框架,咱们将使用一个名为 Cargo 的工具。它是 Rust 的包管理器,能够将它看做是 Rust 工具的 NPM(对于 Javascript 开发者)或 Bundler(对于 Ruby 开发者)。
打开你的终端,进入你想要存储源代码的路径下,而后输入下面的代码。
$ cargo init kt
复制代码
这将会建立一个名为 kt
的目录,该目录下已经有咱们应用程序的基本结构了。
若是咱们 cd
到该目录中,咱们将看到这个目录结构。并且,方便的是,这个项目已经默认初始化了 git。真是太好了!
$ cd kt/
|
.git/
|
.gitignore
|
Cargo.toml
|
src/
复制代码
Cargo.toml
文件包含了咱们的应用程序的基本信息和依赖信息。一样地,能够把它看作应用程序的 package.json
或者 Gemfile
文件。
src/
目录包含了应用程序的源文件,咱们能够看到其中只有一个 main.rs
文件。检查文件的内容,咱们能够看到其中只有一个 main
函数。
fn main() {
println!("Hello, world!");
}
复制代码
试试构建这个项目。因为没有外部依赖,它应该会构建得很是快。
$ cargo build
Compiling kt v0.1.0 (/Users/jeremie/Development/kitty)
Finished dev [unoptimized + debuginfo] target(s) in 2.82s
复制代码
在开发模式下,你能够经过调用 cargo run
来执行二进制文件(用 cargo run --- my_arg
来传递命令行参数)。
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/kt`
Hello, world!
复制代码
恭喜你,你经过刚才的步骤已经建立并运行了你的第一个 Rust 应用程序了!🎉
正如我以前在文章中所说的,咱们正在尝试构建一个简化版的 cat
命令。咱们的目标是模拟 cat
的行为,运行 kt myfile.txt
命令以后,在终端输出文件内容。
咱们原本能够本身处理参数的解析过程,但幸运的是,一个 Rust 工具能够帮咱们简化这个过程,它就是 Clap。
这是一个高性能的命令行参数解析器,它让咱们管理命令行参数变得很简单。
使用这个工具的第一步是打开 Cargo.toml
文件,并在其中添加指定的依赖项。若是你从未处理过 .toml
文件也不要紧,它与 Windows 系统中的 .INI
文件极其类似。这种文件格式在 Rust 中是很常见的。
在这个文件中,你将看到有一些信息已经填充好了,好比做者、版本等等。咱们只须要在 [dependencies]
下添加依赖项就好了。
[dependencies]
clap = "~2.32"
复制代码
保存文件后,咱们须要从新构建项目,以便可以使用依赖库。即便 cargo
下载了除 clap
之外的文件也不用担忧,这是因为 clap
也有其所需的依赖关系。
$ cargo build
Updating crates.io index
Downloaded clap v2.32.0
Downloaded atty v0.2.11
Downloaded bitflags v1.0.4
Downloaded ansi_term v0.11.0
Downloaded vec_map v0.8.1
Downloaded textwrap v0.10.0
Downloaded libc v0.2.48
Downloaded unicode-width v0.1.5
Downloaded strsim v0.7.0
Compiling libc v0.2.48
Compiling unicode-width v0.1.5
Compiling strsim v0.7.0
Compiling bitflags v1.0.4
Compiling ansi_term v0.11.0
Compiling vec_map v0.8.1
Compiling textwrap v0.10.0
Compiling atty v0.2.11
Compiling clap v2.32.0
Compiling kt v0.1.0 (/home/jeremie/Development/kt)
Finished dev [unoptimized + debuginfo] target(s) in 33.92s
复制代码
以上就是须要配置的内容,接下来咱们能够动手,写一些代码来读取咱们的第一个命令行参数。
打开 main.rs
文件。咱们必须显式地声明咱们要使用 Clap 库。
extern crate clap;
use clap::{Arg, App};
fn main() {}
复制代码
extern crate
关键字用于导入依赖库,你只需将其添加到主文件中,应用程序的任何源文件就均可以引用它了。use
部分则是指你将在这个文件中使用 clap
的哪一个模块。
Rust 模块(module)的简要说明:
Rust 有一个模块系统,可以以有组织的方式重用代码。模块是一个包含函数或类型定义的命名空间,你能够选择这些定义是否在其模块外部可见(public/private)。—— Rust 文档
这里咱们声明的是咱们想要使用 Arg
和 App
模块。咱们但愿咱们的应用程序有一个 FILE
参数,它将包含一个文件路径。Clap 能够帮助咱们快速实现该功能。这里使用了一种链式调用方法的方式,这是一种使人很是愉悦的方式。
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
}
复制代码
再次编译并执行,除了变量 matches
上的编译警告(对于 Ruby 开发者来讲,能够在变量前面加上 _
,它会告诉编译器该变量是可选的),它应该不会输出太多其余信息。
若是你向应用程序传递 -h
或者 -V
参数,程序会自动生成一个帮助信息和版本信息。我不知道你如何看待这个事情,但我以为它 🔥🔥🔥。
$ cargo run -- -h
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/kt -h`
kt 0.1.0
Jérémie Veillet. jeremie@example.com
A drop-in cat replacement written in Rust
USAGE:
kt [FILE]
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
ARGS:
<FILE> File to print.
$ cargo run --- -V
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running target/debug/kt -V
kt 0.1.0
复制代码
咱们还能够尝试不带任何参数,启动程序,看看会发生什么。
$ cargo run --
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/kt`
复制代码
什么都没有发生。这是每次构建命令行工具时应该发生的默认行为。我认为不向应用程序传递任何参数就永远不该该触发任何操做。即便有时候这并不正确,可是在大多数状况下,永远不要执行用户从未打算执行的操做。
如今咱们已经有了参数,咱们能够深刻研究如何捕获这个命令行参数并在标准输出中显示一些内容。
要实现这一点,咱们可使用 clap
中的 value_of
方法。请参考文档来了解该方法是怎么运做的。
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
println!("Value for file argument: {}", file);
}
}
复制代码
此时,你能够运行应用程序并传入一个随机字符串做为参数,在你的控制台中会回显该字符串。
$ cargo run -- test.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/kt test.txt`
Value for file argument: test.txt
复制代码
请注意,目前咱们实际上没有对该文件是否存在进行验证。那么咱们应该怎么实现呢?
有一个标准库可让咱们检查一个文件或目录是否存在,使用方式很是简单。它就是 std::path
库。它有一个 exists
方法,能够帮咱们检查文件是否存在。
如前所述,使用 use
关键字来添加依赖库,而后编写以下代码。你能够看到,咱们使用 If-Else
条件控制在输出中打印一些文本。println!
方法会写入标准输出 stdout
,而 eprintln!
会写入标准错误输出 stderr
。
extern crate clap;
use clap::{Arg, App};
use std::path::Path;
use std::process;
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
println!("Value for file argument: {}", file);
if Path::new(&file).exists() {
println!("File exist!!");
}
else {
eprintln!("[kt Error] No such file or directory.");
process::exit(1); // 程序错误终止时的标准退出码
}
}
}
复制代码
咱们快要完成了!如今咱们须要读取文件的内容并将结果显示在 stdout
中。
一样,咱们将使用一个名为 File
的标准库来读取文件。咱们将使用 open
方法读取文件的内容,而后将其写入一个字符串对象,该对象将在 stdout
中显示。
extern crate clap;
use clap::{Arg, App};
use std::path::Path;
use std::process;
use std::fs::File;
use std::io::{Read};
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
if Path::new(&file).exists() {
println!("File exist!!");
let mut f = File::open(file).expect("[kt Error] File not found.");
let mut data = String::new();
f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
println!("{}", data);
}
else {
eprintln!("[kt Error] No such file or directory.");
process::exit(1);
}
}
}
复制代码
再次构建并运行此代码。恭喜你!咱们如今有一个功能完整的工具了!🍾
$ cargo build
Compiling kt v0.1.0 (/home/jeremie/Development/kt)
Finished dev [unoptimized + debuginfo] target(s) in 0.70s
$ cargo run -- ./src/main.rs
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/kt ./src/main.rs`
File exist!!
extern crate clap;
use clap::{Arg, App};
use std::path::Path;
use std::process;
use std::fs::File;
use std::io::{Read};
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
if Path::new(&file).exists() {
println!("File exist!!");
let mut f = File::open(file).expect("[kt Error] File not found.");
let mut data = String::new();
f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
println!("{}", data);
}
else {
eprintln!("[kt Error] No such file or directory.");
process::exit(1);
}
}
}
复制代码
咱们的应用程序现能够接收一个参数并在 stdout
中显示结果。
咱们能够稍微调整一下整个打印阶段的性能,方法是用 writeln!
来代替 println!
。这在 Rust 输出教程中有很好的解释。在此过程当中,咱们能够清理一些代码,删除没必要要的打印,并对可能的错误场景进行微调。
extern crate clap;
use clap::{Arg, App};
use std::path::Path;
use std::process;
use std::fs::File;
use std::io::{Read, Write};
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
if Path::new(&file).exists() {
match File::open(file) {
Ok(mut f) => {
let mut data = String::new();
f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
let stdout = std::io::stdout(); // 获取全局 stdout 对象
let mut handle = std::io::BufWriter::new(stdout); // 可选项:将 handle 包装在缓冲区中
match writeln!(handle, "{}", data) {
Ok(_res) => {},
Err(err) => {
eprintln!("[kt Error] Unable to display the file contents. {:?}", err);
process::exit(1);
},
}
}
Err(err) => {
eprintln!("[kt Error] Unable to read the file. {:?}", err);
process::exit(1);
},
}
}
else {
eprintln!("[kt Error] No such file or directory.");
process::exit(1);
}
}
}
复制代码
$ cargo run -- ./src/main.rs
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/kt ./src/main.rs`
extern crate clap;
use clap::{Arg, App};
use std::path::Path;
use std::process;
use std::fs::File;
use std::io::{Read, Write};
fn main() {
let matches = App::new("kt")
.version("0.1.0")
.author("Jérémie Veillet. jeremie@example.com")
.about("A drop in cat replacement written in Rust")
.arg(Arg::with_name("FILE")
.help("File to print.")
.empty_values(false)
)
.get_matches();
if let Some(file) = matches.value_of("FILE") {
if Path::new(&file).exists() {
match File::open(file) {
Ok(mut f) => {
let mut data = String::new();
f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
let stdout = std::io::stdout(); // 获取全局 stdout 对象
let mut handle = std::io::BufWriter::new(stdout); // 可选项:将 handle 包装在缓冲区中
match writeln!(handle, "{}", data) {
Ok(_res) => {},
Err(err) => {
eprintln!("[kt Error] Unable to display the file contents. {:?}", err);
process::exit(1);
},
}
}
Err(err) => {
eprintln!("[kt Error] Unable to read the file. {:?}", err);
process::exit(1);
},
}
}
else {
eprintln!("[kt Error] No such file or directory.");
process::exit(1);
}
}
}
复制代码
咱们完成了!咱们经过约 45 行代码就完成了咱们的简化版 cat
命令 🤡,而且它表现得很是好!
那么构建这个应用程序并将其安装到文件系统中要怎么作呢?向 cargo 寻求帮助吧!
cargo build
接受一个 ---release
标志位,以便咱们能够指定咱们想要的可执行文件的最终版本。
$ cargo build --release
Compiling libc v0.2.48
Compiling unicode-width v0.1.5
Compiling ansi_term v0.11.0
Compiling bitflags v1.0.4
Compiling vec_map v0.8.1
Compiling strsim v0.7.0
Compiling textwrap v0.10.0
Compiling atty v0.2.11
Compiling clap v2.32.0
Compiling kt v0.1.0 (/home/jeremie/Development/kt)
Finished release [optimized] target(s) in 28.17s
复制代码
生成的可执行文件位于该子目录中:./target/release/kt
。
你能够将这个文件复制到你的 PATH
环境变量中,或者使用一个 cargo 命令来自动安装。应用程序将安装在 ~/.cargo/bin/
目录中(确保该目录在 ~/.bashrc
或 ~/.zshrc
的 PATH
环境变量中)。
$ cargo install --path .
Installing kt v0.1.0 (/home/jeremie/Development/kt)
Finished release [optimized] target(s) in 0.03s
Installing /home/jeremie/.cargo/bin/kt
复制代码
如今咱们能够直接在终端中使用 kt
命令调用咱们的应用程序了!\o/
$ kt -V
kt 0.1.0
复制代码
咱们建立了一个仅有数行 Rust 代码的命令行小工具,它接受一个文件路径做为输入,并在 stdout
中显示该文件的内容。
你能够在这个 GitHub 仓库中找到这篇文章中的全部源代码。
轮到你来改进这个工具了!
-n
选项)。ENTER
键来显示其他部分。kt myfile.txt myfile2.txt myfile3.txt
这样的语法一次性打开多个文件。不要犹豫,告诉我你用它作了什么!😎
特别感谢帮助修订这篇文章的 Anaïs 👍
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。