Paths

Create and Manipulate Paths

std cat~filesystem

std::path↗ provides types and functions for working with file paths. Its two main types are Path and PathBuf. The relationship between them is directly analogous to str and String:

  • Path is an unsized, immutable sequence of characters representing a file path. Because it is unsized, you almost always see it used as a borrowed slice: &Path. It is a "view" of a path string. It's efficient because it doesn't require a memory allocation.
  • PathBuf is an owned, mutable, growable buffer containing a file path. Use PathBuf when you need to build or modify a path.

Note that Path and PathBuf are thin wrappers around OsString and OsStr respectively, meaning that they work directly on strings according to the local platform's path syntax. Path methods that do not access the filesystem, such as Path::starts_with and Path::ends_with, are case sensitive no matter the platform or filesystem. An exception to this is made for Windows drive letters.

The following example demonstrates the use of Path and PathBuf:

use std::path::Path;
use std::path::PathBuf;

/// Displays the path and extract file names, extensions, and other path
/// components from a path.
///
/// Note that many path-handling functions use `AsRef<Path>` as their input
/// type, and therefore generically accept `&Path`, `&PathBuf`, `&str`,
/// `&OsStr`...
fn inspect_path<P: AsRef<Path>>(path: P) {
    let p: &Path = path.as_ref();

    // The `display()` method safely prints paths that may contain non-Unicode
    // data. This may perform lossy conversion, depending on the platform.
    println!("\nInspecting: {}", p.display());

    // `parent` returns the parent directory as an `Option<&Path>`:
    if let Some(parent) = p.parent() {
        println!("  Parent: {}", parent.display());
    }

    // Returns the last component of the path (e.g., `file.txt`) as an
    // `Option<&OsStr>`. `file_name` is a misnomer for a path to a directory:
    if let Some(filename) = p.file_name() {
        println!("  Last Component: {filename:?}");
    }

    //  Returns the file extension (without the leading dot) as an
    // `Option<&OsStr>`:
    if let Some(ext) = p.extension() {
        println!("  Extension: {ext:?}");
    }

    print!("  All Components:\n    ");
    for component in p.components() {
        print!(" {component:?}");
    }
    println!();
}

fn main() {
    // Create a `Path` from a string literal:
    let _path = Path::new("./temp/documents/report.txt");

    // Create an owned `PathBuf` to build a new path:
    let mut path_buffer = PathBuf::from("./temp");

    // Manipulate the `PathBuf`:
    path_buffer.push("downloads"); // Append a directory.
    path_buffer.push("archive.zip"); // Append a filename.

    println!("Built path: {}", path_buffer.display());

    // You can change the extension:
    path_buffer.set_extension("tar.gz");
    println!("Changed extension: {}", path_buffer.display());

    // You can remove the last component:
    path_buffer.pop(); // Removes "archive.tar.gz"
    println!("Popped component: {}", path_buffer.display());

    inspect_path(&path_buffer);

    // You can also use `collect` to build a `PathBuf` from its parts:
    let path_buffer: PathBuf = ["/", "home", "user"].iter().collect();
    println!("Another path: {}", path_buffer.display());
}

Canonicalize a Path

std cat~filesystem

The following returns the canonical, absolute form of a path with all intermediate components normalized and symbolic links resolved:

use std::fs;
use std::path::PathBuf;

fn main() -> std::io::Result<()> {
    // Create a directory for this example.
    let path = "./temp/examples";
    fs::create_dir_all(path)?;

    // Canonicalize a path.
    // The path must exist.
    let normalized_path: PathBuf =
        fs::canonicalize("./temp/examples/../examples")?;
    println!("Canonical path: {}", normalized_path.display());

    // You may also use `Path::canonicalize`:
    // let path = std::path::Path::new("/tmp/xmpl/../xmpl/bar.rs");
    // assert_eq!(path.canonicalize().unwrap(),
    // PathBuf::from("/tmp/xmpl/bar.rs"));

    Ok(())
}

Ensure your Paths are UTF-8

The camino crate provides an alternative to the standard Path and PathBuf types that are guaranteed to be UTF-8 encoded. While Rust strings are UTF-8, file paths on many operating systems are not, which can lead to complex error handling. camino ensures your path types are always valid UTF-8, just like your strings:

use std::path::PathBuf;

use camino::Utf8PathBuf;

fn main() {
    // The standard `PathBuf` can contain non-UTF-8 data:
    let mut path = PathBuf::from("/home/user");
    path.push("résumé.pdf"); // This might be non-UTF-8 on some systems.

    // .to_str() returns an Option, which can be `None`.
    match path.to_str() {
        Some(s) => println!("The path is: {s}"),
        None => println!("Path is not valid UTF-8."),
    }

    // `Utf8Path` and `Utf8PathBuf` work just like their standard library
    // counterparts but guarantee their content is valid UTF-8, eliminating
    // the need to check.

    // Create a new Utf8PathBuf. The `from` method works like the standard one.
    let mut path = Utf8PathBuf::from("data/files/résumé");
    println!("Initial path: {path}");

    // Push a new component onto the path.
    // This is guaranteed to be UTF-8.
    path.push("résumé-2025.txt");
    println!("Final path: {path}");

    // You can directly use the path as a `&str` without any `to_str()` call.
    if path.as_str().ends_with(".txt") {
        println!("This is a text file.");
    }

    // You can also get e.g. the file name directly:
    if let Some(file_name) = path.file_name() {
        println!("File name is: {file_name}");
    }
}

References

  • Directories.
  • Directory Traversal.
  • Reading and Writing Files.
  • Symbolic Links.