S Salted的笔记
Tab折叠当前
Shift+Tab全局折叠
/搜索
点击圆点折叠

Ch1

更新2026-06-19 章节24
cargo创建并运行项目§

println!() 是一个宏 macro , 它本质上是一段能生成代码的代码. 所有以 ! 结尾的"函数"都是macro,

mkdir -p parent/child # -p 选项能在创建child目录前先创建parent 
cargo new 

不显示编译信息并运行

cargo run --quiet
# Or: 
cargo run -q 

默认情况下cargo会创建一个debug版本的程序,

测试§

新建目录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'
添加依赖§

为了让我们自己写的程序能像命令一样用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
;;
退出码§

命令行程序会返回一个退出码来说明程序运行结束的状态. 有一个命令 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

测试输出结果§

假设要对 src/main.rs 的输出进行测试:

fn main(){
    println!("hello");
}
fn runs() {
    let mut cmd = Command::cargo_bin("hellorust").unwrap();
    cmd.assert().success().stdout("hello\n") ;
}
Ch2 echo§
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%            
获取命令行参数§
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"] }
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
根据 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" }) ;
}
编写集成测试§

为了进行测试, 除了使用 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"));
}
和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>> ;
  • () 表示 Okunit 类型
  • 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

Ch3 cat§
Ch4 head§
Ch5 wc§
Ch6 uniq§
Ch7 find§
Ch8 cut§
Ch9 grep§
Ch10 comm§
Ch11 tail§
Ch12 fortune§
Ch13 cal§
Ch14 ls§