rust-cli
Table of Contents
1. Ch1
1.1. cargo创建并运行项目
println!()
是一个宏 macro
, 它本质上是一段能生成代码的代码. 所有以 !
结尾的"函数"都是macro,
mkdir -p parent/child # -p 选项能在创建child目录前先创建parent
cargo new
不显示编译信息并运行
cargo run --quiet # Or: cargo run -q
默认情况下cargo会创建一个debug版本的程序,
1.2. 测试
新建目录tests
$ tree -L 2 . ├── Cargo.lock ├── Cargo.toml ├── hello │ └── src ├── src │ ├── main │ ├── main.rs │ └── part2.rs ├── target │ ├── CACHEDIR.TAG │ ├── debug │ ├── rls │ └── tmp └── tests └── cli.rs
#[test] fn works() { assert!(true); }
use std::process::Command; #[test] fn runs() { let mut cmd = Command::new("ls"); let res = cmd.output(); // 返回类型为 Result assert!(res.is_ok()); assert!(false); }
启动测试:
cargo run # shell M-x rust-test # emacs
只有环境变量中记录的命令才能直接运行:
查看环境变量, 并用 tr
将 :
替换为换行符
echo $PATH | tr : '\n'
1.3. 添加依赖
为了让我们自己写的程序能像命令一样用Command的方式调用, 我们需要一个包 assert_cmd
,因为它只是在测试中被使用,因此将它加入到 [dev-dependencies]
下:
... [dependencies] [dev-dependencies] assert_cmd = "1"
cargo build
use assert_cmd::Command; #[test] fn runs() { let mut cmd = Command::cargo_bin("hellorust").unwrap(); cmd.assert().success(); }
cargobin创建一个运行hellorust程序的命令, 并返回一个Result类型的值, unwrap()
尝试将Result中Ok的内容取出来, 若不是Ok, 则会引发panic()
pub enum Result<T, E> { Ok(T), Err(E), }
(*OCaml*) type ('t,'e) result = Ok of 't | Err of 'e ;;
1.4. 退出码
命令行程序会返回一个退出码来说明程序运行结束的状态.
有一个命令 true
总是将退出码置为0
true echo $? 0
类似地, false
命令总是将退出码置为1
我们可以自己写一个 true
创建 src/bin/true.rs
$ tree src/
src/
├── bin
│ └── true.rs
└── main.rs
fn main() { std::process::exit(0); }
cargo run --quite --bin true
并对其进行测试:
#[test] fn true_ok() { let mut cmd = Command::cargo_bin("true").unwrap(); cmd.assert().success(); }
注: rust的test不一定会按照顺序执行, 因为rust本身是一本并发安全的语言,它可以并行运行多个测试.
可以使它只用一个线程进行测试:
cargo test --test-threads=1
rust中的程序默认以0作为退出码, 因此true.rs可以写成:
fn main() {}
同理编写 false.rs
并对其进行测试
fn main(){ std::process::exit(1); }
也能用 abort()
实现退出码为1
fn main (){ std::process::abort(); }
cargo run -q --bin false
fn false_not_ok() { let mut cmd = Command::cargo_bin("false").unwrap(); cmd.assert().failure(); }
cargo test
退出码使得程序能够用 &&
组合起来, 当中间遇到退出码非0时, 后续的命令不会被执行. eg : false && ls
1.5. 测试输出结果
假设要对 src/main.rs
的输出进行测试:
fn main(){ println!("hello"); }
fn runs() { let mut cmd = Command::cargo_bin("hellorust").unwrap(); cmd.assert().success().stdout("hello\n") ; }
2. Ch2 echo
2.1. echo的行为
echo hello
hello
echo "hello world" hello world
echo hello world # 传入了两个参数 hello world
echo会在字符串末尾自动添加换行, 因此这里有两次换行
echo "a\n" a
这里只有一次换行, 因为-n选项使得末尾换行被替换为 '\c'
echo -n "a\n" a
echo -n "hello" hello% echo -n "a" a%
2.2. 获取命令行参数
fn main() { println!("{:?}", std::env::args()); }
{}
是一个占位符, 只有实现了 std::fmt:Display
的对象才能用它打印.
在这里不能用, 而是要使用 {:?}
来输出 debug 版本的struct
# inner: 后面的就是struct中的内容 ~/src/rust-learning/echor $ cargo run -q Args { inner: ["target/debug/echor"] }
$ cargo run -q arg1 hello Args { inner: ["target/debug/echor", "arg1", "hello"] }
但是加入我们希望传入一个选项参数, -n
会被当成是cargo的参数
cargo run -n hello
因此需要用 --
来指明cargo选项参数的结束:
cargo run -- -n hello
$ cargo run -q -- -n -q hello Args { inner: ["target/debug/echor", "-n", "-q", "hello"] }
2.3. 用 clap
解析命令行参数
[dependencies] clap = "2"
cargo build
查看文件大小
du -shc .
use clap::App; fn main() { let _matches = App::new("echor") // 应用名 .version("0.1.0") .author("sun") .about("echo in rust") .get_matches() ; // 解析命令行参数 }
$ cargo run -q -- -h echor 0.1.0 sun echo in rust USAGE: echor FLAGS: -h, --help Prints help information -V, --version Prints version information
use clap::{App,Arg}; fn main() { // println!("{:?}", std::env::args()); let matches = App::new("echor") // 应用名 .version("0.1.0") .author("sun") .about("echo in rust") .arg(Arg::with_name("test") // 作为map的key, 用于取出值 .value_name("TEXT") .help("input test") .required(true) // 是否是必须的参数 .min_values(1), // 且此参数至少要有一个 ) .arg(Arg::with_name("omit_newline") .short("n") .help("don't print newline") .takes_value(false), // 无需为此选项传值 ) .get_matches() ; // 解析命令行参数 println!("{:#?}", matches); //用换行和缩进进行打印 }
$ cargo run -q -- -n hello world ArgMatches { args: { "omit_newline": MatchedArg { occurs: 1, indices: [ 1, ], vals: [], }, "test": MatchedArg { occurs: 2, indices: [ 2, 3, ], vals: [ "hello", "world", ], }, }, subcommand: None, usage: Some( "USAGE:\n echor [FLAGS] <TEXT>...", ), }
$ cargo run -q -- -h echor 0.1.0 sun echo in rust USAGE: echor [FLAGS] <TEXT>... FLAGS: -h, --help Prints help information -n don't print newline -V, --version Prints version information ARGS: <TEXT>... input test
2.4. 根据 with_name()
取出参数值
ArgMatches::values_of -> Option<Values> // Values是迭代器 ArgMatches::values_of_lossy -> Option<Vec<String>>
取出必需的 "text" 参数 :
matches.values_of_lossy("text").unwrap();
对可选的 -n
参数, 要先判断其是否存在
matches.is_present("omit_newline");
为了让输出的结果h之间每个都恰好间隔以后一个空格, 我们需要用到 Vec::join
函数 :
let v = vec!["hello","world"]; println!("{}", v.join("@")) ;
hello@world
use clap::{App,Arg}; fn main() { let matches = App::new("echor") // 应用名 .version("0.1.0") .author("sun") .about("echo in rust") .arg(Arg::with_name("test") // 作为map的key, 用于取出值 .value_name("TEXT") .help("input test") .required(true) // 是否是必须的参数 .min_values(1), // 且此参数至少要有一个 ) .arg(Arg::with_name("omit_newline") .short("n") .help("don't print newline") .takes_value(false), // 无需为此选项传值 ) .get_matches() ; // 解析命令行参数 let text = matches.values_of_lossy("text").unwrap(); let is_newline = matches.is_present("omit_newline"); print!("{}{}", text.join(" "), if is_newline {""} else {"\n" }) ; }
2.5. 编写集成测试
为了进行测试, 除了使用 assert_cmd
之外, 还要使用 predicates
, 即: "谓词".
[dev-dependencies] assert_cmd = "2" predicates = "2"
因为有的时候测试应满足的条件不是简单地判断是否等于某个值, 比如说输出中应包含了 "USAGE" 这个字符串. 这时候就需要使用"谓词".
use assert_cmd::Command; use predicates::prelude::* ; #[test] fn dies_no_args() { let mut cmd = Command::cargo_bin("echor").unwrap(); cmd.assert().failure().stderr(predicate::str::contains("USAGE") ); }
另外有一个技巧就是为一组测试的函数名用相同的前缀, 例如 dies
这样可以使cargo test只运行这些包含了前缀的测试:
cargo test dies
用 .arg()
传入参数进行测试:
#[test] fn one_arg(){ let mut cmd = Command::cargo_bin("echor").unwrap(); cmd.arg("hello").assert().success().stdout(predicate::str::contains("hello")); }
2.5.1. 和echo的输出进行对比
首先要生成echo的结果:
#!/bin/bash OUTDIR="tests/expected" # 注意中括号之间的空格 [[ ! -d "$OUTDIR" ]] && mkdir -p "$OUTDIR" # 判断是否存在此目录, 不存在则创建 echo "hello there" > $OUTDIR/hello1 echo "hello" "there" > $OUTDIR/hello2 echo -n "hello there" > $OUTDIR/hello1n echo -n "hello" "there" > $OUTDIR/hello2n
然后分别编写测试
use std::fs; #[test] fn hello1(){ let outfile = "tests/expected/hello1" ; let expected = fs::read_to_string(outfile).unwrap(); let mut cmd = Command::cargo_bin("echor").unwrap(); cmd.arg("hello there").assert().success().stdout(expected); }
在上面的所有代码中, 我们都直接使用了 unwrap()
来取出Result中的OK值, 但这也默认了程序的结果始终都是正常的. 这样的假设当然是不合理的, 因此我们要创建一个Result类型:
// TestResult = Ok of unit | Err of Box<dyn std::error::Error> type TestResult = Result<(), Box<dyn std::error::Error>> ;
()
表示Ok
是unit
类型Box
表示这是一个指向堆中内存的指针dyn
表示对std::error::Error
进行的方法调用是动态分发的(多态)
之前所有的测试函数的返回值都是 unit
类型, 现在用 TestResult
来取代它. 之前用 unwrap()
来对 Ok
值进行解包, 并当遇到 Err
类型时触发 panic
使程序挂掉.
现在除了用 TestResult
作为返回类型, 还用 ?
来取代 unwrap
? 同样是匹配 Ok
/ Err
的语法糖, 当为 Ok
时, 将其中的值取出来, 当为 Err
时, 会提前 return Err<XX>
, 其中的类型是函数的返回类型决定的: ->Result<OO,XX>
. 这点和 unwrap()
不同的, 不会直接触发 panic