文件系统和进程

目录

再看看读取文件

第 1 部分的末尾,我展示了如何读取整个文件到一个字符串。 自然,这并不总是一个好法子,所以,现在介绍下如何逐行读取文件。

fs::File实现了io::Read,这是一个具备可读性的 trait 。 这个 trait 定义了一个能填充u8切片字节的read方法 - 唯一 要求 的方法,还免费提供一些方法,很像 Iterator。 您可以使用read_to_end填充可读的内容 到 字节 Vec, 还有read_to_string可以填充到 一个 string - 必须是 utf-8 编码。

这是一个’原始’读取,没有缓冲区。 对于缓冲性读取, 我们有io::BufRead trait,给了我们 read_line 和 一个lines迭代器。io::BufReader将给 任何 具备可读性的类型 提供io::BufRead实现。

fs::File 实现了io::Write

确保所有这些 traits 可用的最简单方法是,use std::io::prelude::*


# #![allow(unused_variables)]
#fn main() {
use std::fs::File;
use std::io;
use std::io::prelude::*;

fn read_all_lines(filename: &str) -> io::Result<()> {
    let file = File::open(&filename)?;

    let reader = io::BufReader::new(file);// 实现`io::BufRead`

    for line in reader.lines() {
        let line = line?;
        println!("{}", line);
    }
    Ok(())
}
#}

这里的let line = line?看起来可能有点奇怪。迭代器返回的line实际上是一个io::Result<String>,我们用?解开它。因为在迭代过程中可能 出现错误,如: I/O 错误,不是 utf-8 的字节块,等等。

lines作为一个迭代器,可以直接使用collect从一个文件读取为一个字符串向量,或者用enumerate迭代器打印带行号的 line。

然而,这并不是读取 所有行 的最有效方式,因为每行都要分配一个新字符串,有成本。使用read_line效率更高,虽然更难看些。请注意,返回的行是包含换行符,可以使用trim_right进行移除。


# #![allow(unused_variables)]
#fn main() {
    let mut reader = io::BufReader::new(file);
    let mut buf = String::new();
    while reader.read_line(&mut buf)? > 0 {
        {
            let line = buf.trim_right();
            println!("{}", line);
        }
        buf.clear();
    }
#}

分配内存的举动少得很多,因为字符串不会释放其分配的内存, clearing{清除} 也只是缓存区; 一旦字符串有足够的容量,不会再有分配。

这是我们使用一个块{}来控制单一借用的情况。linebuf的借用,而这个借用必须在我们修改buf之前完结。Rust 再一次试图阻止我们做一些愚蠢的事情,那就是 我们已经清除了缓冲区 ,访问line。(借用检查者有时会有所限制,Rust 由于”非词汇生命周期{non-lexical lifetimes}”,它会分析代码并看到,在buf.clear()之后line是不使用的。)

完成的不是很漂亮。 虽然我不能给你一个,能返回缓冲区引用的完全迭代器,但我可以给你一些 看起来像 一个迭代器的东西。

首先定义一个泛型结构; 类型参数R是’任意实现 Read 的类型’。结构包含读者{reader} 和我们要借用的缓冲区{buf}。


# #![allow(unused_variables)]
#fn main() {
// file5.rs
use std::fs::File;
use std::io;
use std::io::prelude::*;

struct Lines<R> {
    reader: io::BufReader<R>,
    buf: String
}

impl <R: Read> Lines<R> {
    fn new(r: R) -> Lines<R> {
        Lines{reader: io::BufReader::new(r), buf: String::new()}
    }
    ...
}
#}

然后是next方法。 它返回一个Option- 就像一个迭代器,当它返回None时,迭代器结束。这返回的类型嵌套个Result是因为read_line可能会失败,我们永远不要错失错误。 所以如果失败了,我们把它的错误包进Some<Result>。 否则,文件的自然结束时,它可能读取到零字节 - 而零字节不是错误,只是一个None

此时,缓冲区包含附有换行符 (\n) 的行,修剪掉它,然后打包成字符串切片。


# #![allow(unused_variables)]
#fn main() {
    fn next<'a>(&'a mut self) -> Option<io::Result<&'a str>>{
        self.buf.clear();
        match self.reader.read_line(&mut self.buf) {
            Ok(nbytes) => if nbytes == 0 {
                None // 没有更多行啦!
            } else {
                let line = self.buf.trim_right(); // trim_right的函数签名:`pub fn trim_right(&self) -> &str`
                Some(Ok(line))
            },
            Err(e) => Some(Err(e))
        }
    }
#}

现在,请注意 生命周期 如何工作。 我们需要明确的 生命周期 ,因为 Rust 永远不会让我们,在不知道他们的 生命周期 的情况下,搞到借用的字符串切片。在这里,我们说这个借用的字符串的 生命周期 在self 的生命周期里面。

而且,生命周期的这个签名不兼容Iterator的接口(/trait), 但是如果兼容就很容易出现问题; 考虑到collect试图制作这些字符串切片的 Vec,可这是不能工作的,因为它们都是从 同一个 可变字符串self.buf中借用的! (如果您已 读取了文件的 所有 ,并转换为字符串,而这个字符串的lines迭代器是可以返回字符串切片,因为它们都是借用原始字符串的 不同 部分)。

最终,得到的循环结果更清晰,文件缓冲区对用户是不可见的。


# #![allow(unused_variables)]
#fn main() {
fn read_all_lines(filename: &str) -> io::Result<()> {
    let file = File::open(&filename)?;

    let mut lines = Lines::new(file);
    while let Some(line) = lines.next() {
        let line = line?;
        println!("{}", line);
    }

    Ok(())
}
#}

你甚至可以这样写循环,显式匹配可以从字符串切片拉出来 :


# #![allow(unused_variables)]
#fn main() {
    while let Some(Ok(line)) = lines.next() {
        println!("{}", line)?;
    }
#}

这很诱人,但你在这里抛出一个可能的错误;每当发生错误时,此循环都会静静地停止。 特别是,它将停止在,无法将 line 转换为 utf-8 的第一处位置。适合休闲代码,不适合生产代码!

写进文件

Debug实现处,我们遇到了write!宏,- 它也适用于任何实现了Write的东西。那另一种,是print!:


# #![allow(unused_variables)]
#fn main() {
    let mut stdout = io::stdout();
    ...
    write!(stdout,"answer is {}\n", 42).expect("write failed");
#}

如果 可能 有错误,你必须处理它,不 容易 但可能发生。通常还好,因为如果是文件 I/O,(一般情况下)应该能加个?

但有一个区别: print!为每个写锁定 stdout。 通常是您想要输出的内容,因为若没有锁定,多线程程序会混淆输出,搞笑的方式。 但是,如果是要甩出大量文字,那么write!会更快。

对任意文件,我们用到write!。 在 write_out的结尾, out变量会释放,文件自然关闭。

// file6.rs
use std::fs::File;
use std::io;
use std::io::prelude::*;

fn write_out(f: &str) -> io::Result<()> {
    let mut out = File::create(f)?;
    write!(out,"answer is {}\n", 42)?;
    Ok(())
}

fn main() {
  write_out("test.txt").expect("write failed");
}

如果你关心性能,你需要知道 Rust 文件默认是无缓冲的。 所以每个小的写入请求都会直接进入操作系统,而这会明显变慢。我提到了这一点,因为这种默认设置与其他编程语言不同,并且可能导致令人震惊的发现: Rust 可能有脚本语言遗留的残渣!

io::BufWriterReadio::BufReader,但带有缓冲的Write

文件,路径和目录

这是一个用于在机器上打印 Cargo 目录的小程序。最简单的情况是’~/.cargo’。在一个 Unix shell 环境,所以我们使用env::home_dir,因为它是跨平台的。 (它可能会失败,但没有主目录的计算机, 无论如何都不会打算托管 Rust 工具的。 )

这里,我们创建一个PathBuf,并使用它的push方法,构建完整的文件路径就像 组件。 (这比 用/,\或其他任何东西来要容易得多,老要顾虑系统。)

// file7.rs
use std::env;
use std::path::PathBuf;

fn main() {
    let home = env::home_dir().expect("no home!");
    let mut path = PathBuf::new();
    path.push(home);
    path.push(".cargo");

    if path.is_dir() {
        println!("{}", path.display());
    }
}

一个PathBuf就好像String- 它拥有一组可扩展的方法,但具有专门用于构建路径的方法。 但其大部分功能都来自借用版本的Path,这就像&str。 所以,举个例子,is_dir就是一个Path方法。

这可能听起来像一种继承形式,但魔法Deref trait 的工作方式不同。就像String/&str能一起工作 - PathBuf引用可 包裹{Coerce}Path引用。 (’Coerce’是一个很强的词,但这确实是 Rust ,为你提供转换的几个地方之一。 )


# #![allow(unused_variables)]
#fn main() {
fn foo(p: &Path) {...}
...
let path = PathBuf::from(home);
foo(&path);
#}

PathBufOsString有亲密的关系,它代表我们直接从系统获得的字符串。(相应的,OsString/&OsStr关系. )

这样的字符串不 保证 可以表示为 utf-8! 现实生活是一个复杂的事情,特别是看到’他们为什么这么辛苦’的答案。总而言之,首先有几年的 ASCII 传统编码,以及其他语言的多种特殊编码。 其次,人类语言很复杂。 例如’noël’是 五个 Unicode 代码点!

确实,现代操作系统文件名的大部分都是 Unicode 格式 (Unix 方面的 utf-8 ,Windows 方面的 UTF-16) ,又或者不是。 Rust 必须严格处理这种可能性。 例如,Path有一个as_os_str方法。它返回一个&OsStr,但是to_str方法却是返回一个Option<&str>。随缘!

人们在这一点上遇到了麻烦,因为他们已经过分依赖’string’和’character’作为唯一的必要抽象。 正如爱因斯坦所说, 编程语言必须尽可能简单,但并不简单。 系统语言 需要 区分一个String/&str(拥有与借用: 这也非常方便) ,如果它希望站在标准化的 Unicode 字符串上,那么它需要另一种类型来处理无效 Unicode 的文本 - 因此有了OsString/&OsStr。 请注意,这些类型没有任何有趣的,类似字符串的方法,因为我们本来就不知道无效 Unicode 编码。

但是,人们习惯像处理字符串一样处理文件名,这就是 Rust 使用PathBuf方法操作文件路径,会更容易的原因。

您可以pop连续去除路径组件。 这里我们从程序的当前目录开始:

// file8.rs
use std::env;

fn main() {
    let mut path = env::current_dir().expect("can't access current dir");
    loop {
        println!("{}", path.display());
        if ! path.pop() {
            break;
        }
    }
}
// /home/steve/rust/gentle-intro/code
// /home/steve/rust/gentle-intro
// /home/steve/rust
// /home/steve
// /home
// /

这是一个有用的变化。 我有一个搜索 配置{config} 文件 的程序,其规则是,它可能出现在当前目录的任何子目录中。 所以我创建/home/steve/rust/config.txt,并在/home/steve/rust/gentle-intro/code启动此程序:

// file9.rs
use std::env;

fn main() {
    let mut path = env::current_dir().expect("can't access current dir");
    loop {
        path.push("config.txt");
        if path.is_file() {
            println!("gotcha {}", path.display());
            break;
        } else {
            path.pop();
        }
        if ! path.pop() {
            break;
        }
    }
}
// gotcha /home/steve/rust/config.txt

如此像 git 的工作方式,当它想知道当前的存储库是是什么的时候。

有关文件的详细信息 (其大小,类型等) 被称为它的 元数据。 与往常一样,可能存在错误 - 不仅仅是”找不到”,或是我们没有权限读取此文件。

// file10.rs
use std::env;
use std::path::Path;

fn main() {
    let file = env::args().skip(1).next().unwrap_or("file10.rs".to_string());
    let path = Path::new(&file);
    match path.metadata() {
        Ok(data) => {
            println!("type {:?}", data.file_type());
            println!("len {}", data.len());
            println!("perm {:?}", data.permissions());
            println!("modified {:?}", data.modified());
        },
        Err(e) => println!("error {:?}", e)
    }
}
// type FileType(FileType { mode: 33204 })
// len 488
// perm Permissions(FilePermissions { mode: 436 })
// modified Ok(SystemTime { tv_sec: 1483866529, tv_nsec: 600495644 })

文件的长度 (以字节为单位) 和修改时间很容易解释。 (注意我们可能无法获得这个时间!) 文件类型有方法is_dir,is_fileis_symlink

权限{perissions}是一个有趣的点。 Rust 努力成为跨平台的,所以这是’木桶的最短木条’的例子。 一般来说,你可以查询的仅是,文件是否只读 - ‘权限’概念在 Unix 中被扩展,并为 用户/群组/其他 提供 读/写/可执行的权限。

但是,如果您对 Windows 不感兴趣,那么引入特定于平台的 traits 将至少为我们提供,权限模式位数。 (像往常一样,一个 trait 只有在它可见时才会触发。 ) 然后,应用到程序:


# #![allow(unused_variables)]
#fn main() {
use std::os::unix::fs::PermissionsExt;
...
println!("perm {:o}",data.permissions().mode());
// perm 755
#}

(注意”{:o}”用于打印 八进制)

(Windows 上的文件是否可执行取决于其扩展名。可执行文件的扩展名可以在PATHEXT环境变量找到 - ‘.exe’,’. bat’等等) .

std::fs包含许多用于处理文件的有用功能,例如复制或移动文件,制作符号链接和创建目录。

要查找目录的内容,std::fs::read_dir提供了一个迭代器。 以下是扩展名为”.rs”且大小大于 1024 字节 的所有文件:


# #![allow(unused_variables)]
#fn main() {
fn dump_dir(dir: &str) -> io::Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let data = entry.metadata()?;
        let path = entry.path();
        if data.is_file() {
            if let Some(ex) = path.extension() {
                if ex == "rs" && data.len() > 1024 {
                    println!("{} length {}", path.display(),data.len());
                }
            }
        }
    }
    Ok(())
}
// ./enum4.rs length 2401
// ./struct7.rs length 1151
// ./sexpr.rs length 7483
// ./struct6.rs length 1359
// ./new-sexpr.rs length 7719
#}

显然,read_dir可能会失败 (通常是’找不到’或’没有权限’),但是获取每个新条目时,也可能会失败 (这就像是line迭代器 遍历 缓冲的 reader 的内容)。 另外,我们可能无法获取与条目对应的元数据。 一个文件可能没有扩展名,所以我们也必须检查。

为什么不仅搞出,一个遍历路径的迭代器? 在 Unix 上,是opendir系统调用在起作用,但在 Windows 上,您无法在不获取元数据的情况下,迭代目录的内容。所以,这已是一个相当优雅的妥协方案,它允许跨平台的代码尽可能的高效。

关于感觉到’错误疲劳{error fatigue}’,你可以被原谅。但请注意 错误总是存在 - 这不是 Rust 的新发明。 它只是在努力让你无法忽视它们。 任何操作系统调用都可能失败。

Java 和 Python 等语言会引发异常; 像 Go 和 Lua 这样的语言返回两个值,其中第一个是 结果,第二个是 错误: 像 Rust 也一样,它要考虑到是库函数引发错误的不良。所以才有,这么多错误检查和函数的提前返回。

Rust 使用Result,因为它有两面性(either-or): 你不能同时得到 一个结果 和 错误。?问号运算符使处理错误更加清晰。

进程

一个基本的需求是程序去运行程序,或者 启动进程 。 你的程序可以 启动{launch} 尽可能多的子进程,顾名思义此类进程间有特殊的关系。

使用Command结构,运行子程序很简单,拿到传递给子程序的构建参数:

use std::process::Command;

fn main() {
    let status = Command::new("rustc")
        .arg("-V")
        .status()
        .expect("no rustc?");

    println!("cool {} code {}", status.success(), status.code().unwrap());
}
// rustc 1.15.0-nightly (8f02c429a 2016-12-15)
// cool true code 0

看啊,new收到该程序的名称 (它将查找PATH,如果不是绝对文件名的话),arg增加了一个新的 尾随参数,并且status导致它运行。 这返回一个Result,若为Ok,说明程序运行了,且包含一个退出状态{ExitStatus}。在这种情况下,程序成功,并返回 退出码 0 (使用unwrap是因为,如果程序被信号杀死了,我们不总是得到退出代码)。

如果我们改变了-V-v (一个易犯的错误),导致rustc失败:

error: no input filename given

cool false code 101

所以有三种可能性:

  • 程序不存在,很糟糕,或者我们不允许运行它
  • 程序运行,但没有成功 - 非零退出代码
  • 程序运行,零退出代码。成功!

默认情况下,程序的 stdout 和标准错误流将发送到终端。

我们经常对这种输出非常感兴趣,也就是output方法.

// process2.rs
use std::process::Command;

fn main() {
    let output = Command::new("rustc")
        .arg("-V")
        .output()
        .expect("no rustc?");

    if output.status.success() {
        println!("ok!");
    }
    println!("len stdout {} stderr {}", output.stdout.len(), output.stderr.len());
}
//Ok!
// len stdout 44 stderr 0

status一样,我们的程序会阻塞,直到子进程结束,我们返回三个值 - status (如上),stdout 的内容和标准错误的内容。

捕获到的内容输出是简单的Vec<u8>- 只是字节。 回想一下,我们不能保证,我们从操作系统收到的数据是正确的 utf-8 编码 字符串。 事实上,我们 甚至 不能保证它是一个字符串 - 程序可能会返回任意二进制数据。

如果我们确信输出是 utf-8,那么String::from_utf8将转换这些 Vec 或字节 - 它返回的是一个Result,因为这种转换可能不会成功。 一个更迷糊的函数是String::from_utf8_lossy,能很好地转换,并在转换失败时插入无效的 Unicode标记。

下面是一个使用 shell 来运行程序的有用函数。这使用通常的 shell 机制,将标准错误连接到 stdout。 在 Windows 上 shell 的名字是不同的,但是除此之外的东西可以按预期工作。


# #![allow(unused_variables)]
#fn main() {
fn shell(cmd: &str) -> (String,bool) {
    let cmd = format!("{} 2>&1",cmd);
    let shell = if cfg!(windows) {"cmd.exe"} else {"/bin/sh"};
    let flag = if cfg!(windows) {"/c"} else {"-c"};
    let output = Command::new(shell)
        .arg(flag)
        .arg(&cmd)
        .output()
        .expect("no shell?");
    (
        String::from_utf8_lossy(&output.stdout).trim_right().to_string(),
        output.status.success()
    )
}


fn shell_success(cmd: &str) -> Option<String> {
    let(output,success) = shell(cmd);
    if success {Some(output)} else {None}
}
#}

我修整了右边的任何空格, 所以,如果你说shell("which rustc")的话,您将获得没有任何额外换行的路径。

您可以通过Process控制启动程序的执行, 使用current_dir方法指定它将运行的目录,和它所使用的环境变量env

到目前为止,我们的程序只是等待子进程完成. 如果你使用spawn方法,我们立即返回,可以明确地等待它完成 - 或者在此期间去做其他事情! 这个例子还显示了如何同时抑制 标准错误和标准错误:

// process5.rs
use std::process::{Command,Stdio};

fn main() {
    let mut child = Command::new("rustc")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("no rustc?");

    let res = child.wait();
    println!("res {:?}", res);
}

默认情况下,子进程”继承”父进程的标准输入和输出。 在这里,我们将 孩子的输出控制重定向到”没有”。 这相当于在 Unix shell 中说>/dev/null 2>/dev/null

现在, 你在 Rust 可以使用 shell (sh要么cmd) 来完成这些事情 。 但通过这种方式,您可以完全程序化地控制进程的创建。

例如,如果我们编写的是.stdout(Stdio::piped()),那么孩子的 stdout 就被重定向到管道。那child.stdout就是你可以用来直接读取输出的东西 (例如: 要实现了Read)。 同样,你可以使用.stdin(Stdio::piped())方法,以便您可以写进child.stdin

但,如果我们使用wait_with_output代替wait, 那么它会返回一个Result<Output>,并将孩子的输出,会以一个Vec<u8>,记录到Outputsudout字段,就像之前的一样。

Child结构,也给你一个明确的kill方法。