๐Ÿฆ€ Functional Rust

491: Path and PathBuf Handling

Difficulty: 1 Level: Beginner Platform-aware filesystem paths โ€” like `String`/`&str` but for paths, with OS separator handling.

The Problem This Solves

In Python and JavaScript, file paths are plain strings. `os.path.join("/home/user", "file.txt")` works on Unix but breaks on Windows if you manually concatenate with `/`. The language doesn't enforce the distinction between "a string" and "a path." Rust has dedicated types: `Path` (borrowed, like `&str`) and `PathBuf` (owned, like `String`). They know about platform path separators โ€” `/` on Unix, `\` on Windows. Calling `.join()`, `.parent()`, `.file_name()`, `.extension()` โ€” these work correctly on all platforms without you thinking about it. More importantly, using `Path`/`PathBuf` in function signatures makes your API self-documenting. When a function takes `&Path`, it's clear it expects a filesystem path, not an arbitrary string. You also get protection from path traversal issues โ€” constructing paths with `.join()` won't accidentally interpret `..` as you'd expect with string concatenation, and you can check `.is_absolute()`.

The Intuition

`Path` is to `PathBuf` as `&str` is to `String`. `Path` is a borrowed reference to path data โ€” you pass it around cheaply. `PathBuf` is the owned, growable version you can build up incrementally. The mental model: OCaml uses plain strings for paths. The Python equivalent is `pathlib.Path` โ€” `Path` in Python is roughly `PathBuf` in Rust (it's the owned, mutable version).

How It Works in Rust

use std::path::{Path, PathBuf};

// PathBuf โ€” owned, growable
let mut p = PathBuf::from("/home");
p.push("user");        // /home/user
p.push("docs");        // /home/user/docs
p.push("file.txt");    // /home/user/docs/file.txt

// Path โ€” borrowed view, decompose it
let path = Path::new("/home/user/docs/file.txt");
path.parent()       // Some("/home/user/docs")
path.file_name()    // Some("file.txt")
path.file_stem()    // Some("file")     โ† without extension
path.extension()    // Some("txt")
path.is_absolute()  // true

// Components โ€” iterate path segments
for component in path.components() {
 println!("{:?}", component);
}
// RootDir, Normal("home"), Normal("user"), Normal("docs"), Normal("file.txt")

// join โ€” builds new PathBuf (non-mutating)
let base = Path::new("/home/user");
let full = base.join("projects").join("rust").join("main.rs");
// /home/user/projects/rust/main.rs

// with_extension โ€” change or add extension
Path::new("report.txt").with_extension("pdf");  // report.pdf
Path::new("archive").with_extension("tar.gz");  // archive.tar.gz

// starts_with / ends_with โ€” path prefix/suffix check
path.starts_with("/home")  // true
path.ends_with("file.txt") // true

// Accept &Path in functions โ€” both Path and PathBuf coerce
fn print_ext(p: &Path) {
 if let Some(ext) = p.extension() {
     println!("{}", ext.to_string_lossy());
 }
}
print_ext(path);           // works with &Path
print_ext(&p);             // works with &PathBuf too (auto-coerce)

What This Unlocks

Key Differences

ConceptOCamlRust
Path type`string` (no dedicated type)`Path` (borrowed) / `PathBuf` (owned)
Join path segments`String.concat "/" parts``path.join("segment")`
Get filenameManual `String.rindex_opt``path.file_name()` โ†’ `Option<&OsStr>`
Get extensionManual `String.rindex_opt '.'``path.extension()` โ†’ `Option<&OsStr>`
Get parent dirManual split`path.parent()` โ†’ `Option<&Path>`
Platform separatorsManualAutomatic โ€” `/` on Unix, `\` on Windows
Append segmentString concat`pathbuf.push("segment")` (mutates)
// 491. Path and PathBuf handling
use std::path::{Path, PathBuf};

fn main() {
    // PathBuf โ€” owned, growable
    let mut p = PathBuf::from("/home");
    p.push("user");
    p.push("documents");
    p.push("file.txt");
    println!("path: {}", p.display());

    // Path โ€” borrowed view
    let path = Path::new("/home/user/documents/file.txt");
    println!("parent:    {:?}", path.parent());
    println!("file_name: {:?}", path.file_name());
    println!("stem:      {:?}", path.file_stem());
    println!("extension: {:?}", path.extension());
    println!("is_abs:    {}", path.is_absolute());

    // Components
    for comp in path.components() { println!("  {:?}", comp); }

    // Join
    let base = Path::new("/home/user");
    let full = base.join("projects").join("rust").join("main.rs");
    println!("joined: {}", full.display());

    // Absolute path from relative
    let rel = Path::new("src/main.rs");
    println!("rel parent: {:?}", rel.parent());

    // with_extension
    let p2 = Path::new("file.txt");
    let rs = p2.with_extension("rs");
    println!("with_ext: {}", rs.display());

    // starts_with
    println!("starts /home: {}", path.starts_with("/home"));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn test_join()   { let p=PathBuf::from("/a").join("b").join("c"); assert_eq!(p.to_str().unwrap(),"/a/b/c"); }
    #[test] fn test_ext()    { assert_eq!(Path::new("f.txt").extension().unwrap(),"txt"); }
    #[test] fn test_stem()   { assert_eq!(Path::new("f.txt").file_stem().unwrap(),"f"); }
    #[test] fn test_parent() { assert_eq!(Path::new("/a/b/c").parent().unwrap(),Path::new("/a/b")); }
}
(* 491. Path handling โ€“ OCaml *)
let () =
  let path = "/home/user/documents/file.txt" in
  let parts = String.split_on_char '/' path |> List.filter ((<>) "") in
  Printf.printf "parts: %s\n" (String.concat " | " parts);
  let dir = String.concat "/" ("" :: List.rev (List.tl (List.rev parts))) in
  Printf.printf "dir: %s\n" dir;
  let base = List.nth parts (List.length parts - 1) in
  Printf.printf "file: %s\n" base;
  let ext = match String.rindex_opt base '.' with
    | Some i -> Some (String.sub base (i+1) (String.length base - i - 1))
    | None -> None
  in
  Printf.printf "ext: %s\n" (Option.value ~default:"none" ext)