BBuzzard

Path is to PathBuf what str is to String: a borrowed view and an owned buffer. join and push append components; parent, file_name, file_stem, and extension take paths apart. Paths are OS strings and need not be UTF-8: display() prints them lossily and to_str() returns an Option.

std::fs covers directories (create_dir_all, read_dir, remove_dir_all), copying and renaming, and metadata: file type, length, permissions, and timestamps.

Checking first and acting later races: a file can disappear between exists() and open(). Prefer doing the operation and handling the NotFound error. For walking large trees, the walkdir crate is the usual choice; std handles one level at a time.

Taking a path apart

fn demo_path_parts() {
    println!("-- path parts --");

    let p = Path::new("/var/log/app/server.log.gz");
    println!("parent {:?}", p.parent());
    println!(
        "file_name {:?} stem {:?} ext {:?}",
        p.file_name(),
        p.file_stem(),
        p.extension()
    );
    println!("absolute {} relative {}", p.is_absolute(), Path::new("a/b").is_relative());

    let parts: Vec<_> = p.components().collect();
    println!(
        "{} components, first is root: {}",
        parts.len(),
        parts[0] == Component::RootDir
    );

    let names: Vec<_> = p.iter().map(|s| s.to_string_lossy()).collect();
    println!("iter {names:?}");
    println!(
        "ancestors {:?}",
        p.ancestors().map(|a| a.display().to_string()).collect::<Vec<_>>()
    );
}

Building and changing paths

fn demo_path_building() {
    println!("-- building paths --");

    let mut p = PathBuf::from("dir1");
    p.push("dir2");
    p.push("s.log");
    println!("{}", p.display());
    p.set_extension("txt");
    p.set_file_name("other.md");
    println!("{}", p.display());

    let joined = Path::new("/etc").join("nginx").join("nginx.conf");
    println!(
        "{} with_extension {}",
        joined.display(),
        joined.with_extension("bak").display()
    );

    // Joining an absolute path replaces everything before it.
    println!("join absolute: {}", Path::new("/a/b").join("/c").display());
    println!("strip_prefix {:?}", joined.strip_prefix("/etc"));
    println!(
        "starts_with {} ends_with {}",
        joined.starts_with("/etc"),
        joined.ends_with("nginx.conf")
    );
}

Paths and strings

fn demo_path_strings() {
    println!("-- paths and strings --");

    let p = Path::new("café/menu.txt");
    let s: Option<&str> = p.to_str();
    let lossy = p.to_string_lossy();
    let owned: String = p.display().to_string();
    println!("{s:?} {lossy} {owned}");

    let from_string: PathBuf = String::from("x/y").into();
    println!("{}", from_string.display());

    // Functions should take impl AsRef<Path> so callers can pass &str, String, or Path.
    fn ext_of(p: impl AsRef<Path>) -> Option<String> {
        p.as_ref().extension()?.to_str().map(String::from)
    }

    println!("{:?} {:?}", ext_of("a.rs"), ext_of(PathBuf::from("noext")));
}

Creating and listing directories

fn scratch_dir() -> PathBuf {
    std::env::temp_dir().join(format!("buzzard-48-{}", std::process::id()))
}

fn demo_dirs() -> io::Result<()> {
    println!("-- directories --");

    let root = scratch_dir();
    fs::create_dir_all(root.join("sub/deeper"))?;
    fs::write(root.join("a.txt"), b"a")?;
    fs::write(root.join("b.rs"), b"fn main() {}")?;
    fs::write(root.join("sub/c.txt"), b"ccc")?;

    let mut entries: Vec<_> = fs::read_dir(&root)?.collect::<Result<_, _>>()?;
    entries.sort_by_key(|e| e.file_name());

    for e in &entries {
        let kind = if e.file_type()?.is_dir() { "dir " } else { "file" };
        println!("{kind} {}", e.file_name().to_string_lossy());
    }

    Ok(())
}

A small recursive walk

fn walk(dir: &Path, depth: usize, out: &mut Vec<String>) -> io::Result<()> {
    let mut entries: Vec<_> = fs::read_dir(dir)?.collect::<Result<_, _>>()?;
    entries.sort_by_key(|e| e.file_name());

    for e in entries {
        out.push(format!("{}{}", "  ".repeat(depth), e.file_name().to_string_lossy()));
        if e.file_type()?.is_dir() {
            walk(&e.path(), depth + 1, out)?;
        }
    }

    Ok(())
}

fn demo_walk() -> io::Result<()> {
    println!("-- recursive walk --");

    let mut lines = Vec::new();
    walk(&scratch_dir(), 0, &mut lines)?;
    println!("{}", lines.join("\n"));
    Ok(())
}

Metadata, copy, rename, and errors

fn demo_metadata() -> io::Result<()> {
    println!("-- metadata --");

    let root = scratch_dir();
    let file = root.join("sub/c.txt");
    let meta = fs::metadata(&file)?;
    println!("len {} is_file {} is_dir {}", meta.len(), meta.is_file(), meta.is_dir());
    println!(
        "readonly {} modified known? {}",
        meta.permissions().readonly(),
        meta.modified().is_ok()
    );
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        println!("unix mode {:o}", meta.permissions().mode() & 0o777);
    }

    let copy = root.join("copy.txt");
    let n = fs::copy(&file, &copy)?;
    fs::rename(&copy, root.join("renamed.txt"))?;
    println!(
        "copied {n} bytes, renamed exists? {}",
        root.join("renamed.txt").try_exists()?
    );

    match fs::metadata(root.join("missing")) {
        Err(e) if e.kind() == io::ErrorKind::NotFound => println!("missing: NotFound"),
        other => println!("unexpected {other:?}"),
    }

    let canonical = fs::canonicalize(&root)?;
    println!("canonical is absolute: {}", canonical.is_absolute());
    Ok(())
}

Cleanup

fn demo_cleanup() -> io::Result<()> {
    println!("-- cleanup --");

    let root = scratch_dir();
    fs::remove_file(root.join("a.txt"))?;

    // remove_dir only removes an empty directory.
    println!(
        "remove_dir on non-empty: {:?}",
        fs::remove_dir(&root).map_err(|e| e.kind()).is_err()
    );
    fs::remove_dir_all(&root)?;
    println!("gone? {}", !root.exists());
    Ok(())
}
$ cargo run --bin 48_fs_paths
-- path parts --
parent Some("/var/log/app")
file_name Some("server.log.gz") stem Some("server.log") ext Some("gz")
absolute true relative true
5 components, first is root: true
iter ["/", "var", "log", "app", "server.log.gz"]
ancestors ["/var/log/app/server.log.gz", "/var/log/app", "/var/log", "/var", "/"]
-- building paths --
dir1/dir2/s.log
dir1/dir2/other.md
/etc/nginx/nginx.conf with_extension /etc/nginx/nginx.bak
join absolute: /c
strip_prefix Ok("nginx/nginx.conf")
starts_with true ends_with true
-- paths and strings --
Some("café/menu.txt") café/menu.txt café/menu.txt
x/y
Some("rs") None
-- directories --
file a.txt
file b.rs
dir  sub
-- recursive walk --
a.txt
b.rs
sub
  c.txt
  deeper
-- metadata --
len 3 is_file true is_dir false
readonly false modified known? true
unix mode 644
copied 3 bytes, renamed exists? true
missing: NotFound
canonical is absolute: true
-- cleanup --
remove_dir on non-empty: true
gone? true

Full program

48_fs_paths/main.rs · 212 lines
View on GitHub
// 48. Paths, directories, and metadata
//
// Path is to PathBuf what str is to String: a borrowed view and an owned buffer. join
// and push append components; parent, file_name, file_stem, and extension take paths
// apart. Paths are OS strings and need not be UTF-8: display() prints them lossily and
// to_str() returns an Option.
//
// std::fs covers directories (create_dir_all, read_dir, remove_dir_all), copying and
// renaming, and metadata: file type, length, permissions, and timestamps.
//
// Checking first and acting later races: a file can disappear between exists() and
// open(). Prefer doing the operation and handling the NotFound error. For walking large
// trees, the walkdir crate is the usual choice; std handles one level at a time.
//
// Run: cargo run --bin 48_fs_paths

use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};

// Example 1: taking a path apart.
fn demo_path_parts() {
    println!("-- path parts --");

    let p = Path::new("/var/log/app/server.log.gz");
    println!("parent {:?}", p.parent());
    println!(
        "file_name {:?} stem {:?} ext {:?}",
        p.file_name(),
        p.file_stem(),
        p.extension()
    );
    println!("absolute {} relative {}", p.is_absolute(), Path::new("a/b").is_relative());

    let parts: Vec<_> = p.components().collect();
    println!(
        "{} components, first is root: {}",
        parts.len(),
        parts[0] == Component::RootDir
    );

    let names: Vec<_> = p.iter().map(|s| s.to_string_lossy()).collect();
    println!("iter {names:?}");
    println!(
        "ancestors {:?}",
        p.ancestors().map(|a| a.display().to_string()).collect::<Vec<_>>()
    );
}

// Example 2: building and changing paths.
fn demo_path_building() {
    println!("-- building paths --");

    let mut p = PathBuf::from("dir1");
    p.push("dir2");
    p.push("s.log");
    println!("{}", p.display());
    p.set_extension("txt");
    p.set_file_name("other.md");
    println!("{}", p.display());

    let joined = Path::new("/etc").join("nginx").join("nginx.conf");
    println!(
        "{} with_extension {}",
        joined.display(),
        joined.with_extension("bak").display()
    );

    // Joining an absolute path replaces everything before it.
    println!("join absolute: {}", Path::new("/a/b").join("/c").display());
    println!("strip_prefix {:?}", joined.strip_prefix("/etc"));
    println!(
        "starts_with {} ends_with {}",
        joined.starts_with("/etc"),
        joined.ends_with("nginx.conf")
    );
}

// Example 3: paths and strings.
fn demo_path_strings() {
    println!("-- paths and strings --");

    let p = Path::new("café/menu.txt");
    let s: Option<&str> = p.to_str();
    let lossy = p.to_string_lossy();
    let owned: String = p.display().to_string();
    println!("{s:?} {lossy} {owned}");

    let from_string: PathBuf = String::from("x/y").into();
    println!("{}", from_string.display());

    // Functions should take impl AsRef<Path> so callers can pass &str, String, or Path.
    fn ext_of(p: impl AsRef<Path>) -> Option<String> {
        p.as_ref().extension()?.to_str().map(String::from)
    }

    println!("{:?} {:?}", ext_of("a.rs"), ext_of(PathBuf::from("noext")));
}

fn scratch_dir() -> PathBuf {
    std::env::temp_dir().join(format!("buzzard-48-{}", std::process::id()))
}

// Example 4: creating and listing directories.
fn demo_dirs() -> io::Result<()> {
    println!("-- directories --");

    let root = scratch_dir();
    fs::create_dir_all(root.join("sub/deeper"))?;
    fs::write(root.join("a.txt"), b"a")?;
    fs::write(root.join("b.rs"), b"fn main() {}")?;
    fs::write(root.join("sub/c.txt"), b"ccc")?;

    let mut entries: Vec<_> = fs::read_dir(&root)?.collect::<Result<_, _>>()?;
    entries.sort_by_key(|e| e.file_name());

    for e in &entries {
        let kind = if e.file_type()?.is_dir() { "dir " } else { "file" };
        println!("{kind} {}", e.file_name().to_string_lossy());
    }

    Ok(())
}

fn walk(dir: &Path, depth: usize, out: &mut Vec<String>) -> io::Result<()> {
    let mut entries: Vec<_> = fs::read_dir(dir)?.collect::<Result<_, _>>()?;
    entries.sort_by_key(|e| e.file_name());

    for e in entries {
        out.push(format!("{}{}", "  ".repeat(depth), e.file_name().to_string_lossy()));
        if e.file_type()?.is_dir() {
            walk(&e.path(), depth + 1, out)?;
        }
    }

    Ok(())
}

// Example 5: a small recursive walk.
fn demo_walk() -> io::Result<()> {
    println!("-- recursive walk --");

    let mut lines = Vec::new();
    walk(&scratch_dir(), 0, &mut lines)?;
    println!("{}", lines.join("\n"));
    Ok(())
}

// Example 6: metadata, copy, rename, and errors.
fn demo_metadata() -> io::Result<()> {
    println!("-- metadata --");

    let root = scratch_dir();
    let file = root.join("sub/c.txt");
    let meta = fs::metadata(&file)?;
    println!("len {} is_file {} is_dir {}", meta.len(), meta.is_file(), meta.is_dir());
    println!(
        "readonly {} modified known? {}",
        meta.permissions().readonly(),
        meta.modified().is_ok()
    );
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        println!("unix mode {:o}", meta.permissions().mode() & 0o777);
    }

    let copy = root.join("copy.txt");
    let n = fs::copy(&file, &copy)?;
    fs::rename(&copy, root.join("renamed.txt"))?;
    println!(
        "copied {n} bytes, renamed exists? {}",
        root.join("renamed.txt").try_exists()?
    );

    match fs::metadata(root.join("missing")) {
        Err(e) if e.kind() == io::ErrorKind::NotFound => println!("missing: NotFound"),
        other => println!("unexpected {other:?}"),
    }

    let canonical = fs::canonicalize(&root)?;
    println!("canonical is absolute: {}", canonical.is_absolute());
    Ok(())
}

// Example 7: cleanup.
fn demo_cleanup() -> io::Result<()> {
    println!("-- cleanup --");

    let root = scratch_dir();
    fs::remove_file(root.join("a.txt"))?;

    // remove_dir only removes an empty directory.
    println!(
        "remove_dir on non-empty: {:?}",
        fs::remove_dir(&root).map_err(|e| e.kind()).is_err()
    );
    fs::remove_dir_all(&root)?;
    println!("gone? {}", !root.exists());
    Ok(())
}

fn main() -> io::Result<()> {
    demo_path_parts();
    demo_path_building();
    demo_path_strings();
    demo_dirs()?;
    demo_walk()?;
    demo_metadata()?;
    demo_cleanup()?;
    Ok(())
}