跳转到主内容文件、目录和进程 | 物联网民工

文件、目录和进程

C 程序员写工具或基础库,很快会碰到这些接口:

FILE *fp = fopen(path, "rb");
fread(buf, 1, sizeof(buf), fp);
fclose(fp);

DIR *dir = opendir(path);
system("gzip file.log");
getenv("HOME");

Rust 标准库里对应的入口主要是:

std::fs       -> 文件和目录的便捷操作
std::io       -> Read / Write / BufRead 等 I/O trait
std::env      -> 参数、环境变量、当前目录
std::process  -> 启动子进程、取退出状态和输出

这一篇不展开所有权和 Drop 的细节,只看 C 常用库函数在 Rust 里的第一层替代方式。

读整个文件

C 里读文件通常先 fopen,再循环 fread:

FILE *fp = fopen(path, "rb");
if (!fp) {
    return -1;
}

size_t n = fread(buf, 1, sizeof(buf), fp);
fclose(fp);

Rust 如果只是读完整文件,可以直接用:

let data = std::fs::read(path)?;

std::fs::read 返回 Result<Vec<u8>, std::io::Error>。成功时得到字节,失败时得到错误。

如果读文本:

let text = std::fs::read_to_string(path)?;

这里会检查内容是不是合法 UTF-8。二进制数据用 read,文本用 read_to_string,不要把所有东西都当成 C 的 char *。

逐块读取用 File 和 Read

大文件或流式处理不要一次读进内存。Rust 里打开文件:

use std::fs::File;
use std::io::{self, Read};

fn checksum(path: &str) -> io::Result<u32> {
    let mut file = File::open(path)?;
    let mut buf = [0_u8; 4096];
    let mut sum = 0_u32;

    loop {
        let n = file.read(&mut buf)?;
        if n == 0 {
            break;
        }

        for byte in &buf[..n] {
            sum += *byte as u32;
        }
    }

    Ok(sum)
}

这里的 read 来自 std::io::Read。它和 C 的 read / fread 很像:把数据写进调用方给的 buffer,返回实际读到的字节数。

不同点是:错误走 Result,文件关闭由 File 离开作用域时自动完成。

写文件

C 里:

FILE *fp = fopen(path, "wb");
fwrite(data, 1, len, fp);
fclose(fp);

Rust 写整个文件:

std::fs::write(path, data)?;

如果要逐步写:

use std::fs::File;
use std::io::{self, Write};

fn write_report(path: &str, lines: &[String]) -> io::Result<()> {
    let mut file = File::create(path)?;

    for line in lines {
        writeln!(file, "{line}")?;
    }

    Ok(())
}

Write 提供 write、write_all、flush 等方法。writeln! 可以写到实现了 Write 的目标上。

缓冲 I/O 用 BufReader 和 BufWriter

C 的 FILE * 本身带缓冲。Rust 里的 File 更接近文件句柄;如果需要按行读或减少系统调用,显式包一层缓冲:

use std::fs::File;
use std::io::{self, BufRead, BufReader};

fn print_lines(path: &str) -> io::Result<()> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);

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

    Ok(())
}

写入时:

use std::fs::File;
use std::io::{self, BufWriter, Write};

fn save_lines(path: &str, lines: &[String]) -> io::Result<()> {
    let file = File::create(path)?;
    let mut writer = BufWriter::new(file);

    for line in lines {
        writeln!(writer, "{line}")?;
    }

    writer.flush()?;
    Ok(())
}

第一层可以这样记:需要按行读、频繁小块读写时,用 BufReader / BufWriter。

目录操作

C 里常见 opendir / readdir:

DIR *dir = opendir(path);
struct dirent *ent;

while ((ent = readdir(dir)) != NULL) {
    puts(ent->d_name);
}

closedir(dir);

Rust 遍历目录:

use std::fs;
use std::io;

fn list_dir(path: &str) -> io::Result<()> {
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        println!("{}", entry.path().display());
    }

    Ok(())
}

注意 read_dir(path)? 只表示目录打开成功。循环里的每个 entry 仍然可能失败,所以还有一个 entry?。

常用目录操作:

std::fs::create_dir("logs")?;
std::fs::create_dir_all("logs/app/2026")?;
std::fs::remove_file("old.log")?;
std::fs::remove_dir("empty-dir")?;
std::fs::remove_dir_all("tree")?;

删除目录树要谨慎,和 C 里递归删除一样,错误路径和目标路径都要明确。

路径用 Path 和 PathBuf

C 代码常把路径当 const char *。Rust 里路径类型是:

std::path::Path
std::path::PathBuf

函数参数可以先写成:

use std::path::Path;

fn load_config(path: &Path) -> std::io::Result<String> {
    std::fs::read_to_string(path)
}

调用时:

let text = load_config(Path::new("config.toml"))?;

需要拼路径:

let path = std::path::PathBuf::from("logs").join("app.log");

不要优先用字符串拼接路径。PathBuf::join 更直接,也更能表达“这是文件系统路径”。

环境变量和当前目录

C 里用 getenv:

const char *home = getenv("HOME");

Rust:

let home = std::env::var("HOME");

它返回 Result<String, VarError>。环境变量可能不存在,也可能不是合法 Unicode。

如果要保留操作系统原始字符串语义,用:

let path = std::env::var_os("PATH");

当前目录:

let cwd = std::env::current_dir()?;
std::env::set_current_dir("/tmp")?;

命令行参数在第一篇里已经用过:

let args: Vec<String> = std::env::args().collect();

如果参数可能包含非 Unicode 路径,后续可以用 args_os()。

启动子进程

C 里最简单会用 system:

int rc = system("gzip file.log");

Rust 不要默认拼 shell 字符串。用 std::process::Command:

use std::process::Command;

let status = Command::new("gzip")
    .arg("file.log")
    .status()?;

if !status.success() {
    eprintln!("gzip failed: {status}");
}

如果要拿 stdout:

let output = Command::new("git")
    .args(["rev-parse", "--short", "HEAD"])
    .output()?;

if output.status.success() {
    let text = String::from_utf8_lossy(&output.stdout);
    println!("{text}");
}

Command::new("prog").arg(value) 比拼接 "prog value" 更安全,也更接近 execve 的参数数组模型。

这一层先记住什么

C 常用库函数到 Rust 标准库的第一层对应关系:

fopen/fread whole file    -> std::fs::read / read_to_string
fread loop                -> File + Read
fwrite                    -> File + Write / std::fs::write
FILE * buffering          -> BufReader / BufWriter
opendir/readdir           -> std::fs::read_dir
getenv                    -> std::env::var / var_os
cwd                       -> std::env::current_dir / set_current_dir
system                    -> std::process::Command
return code / errno       -> Result / io::Error / ExitStatus

Rust 标准库没有把这些能力藏起来,只是把边界拆得更明确:文件内容是字节还是文本,路径是字符串还是 Path,错误是返回值还是进程退出状态,子进程参数是数组还是 shell 命令行。

继续阅读