Path

142. Path::absolute — Make a Path Absolute Without Touching the Filesystem

Need an absolute path for a log line, an error message, or a “files will land here” preview — but the file might not exist yet? fs::canonicalize will refuse. std::path::absolute (stable since Rust 1.79) gives you the absolute form without ever opening the disk.

The canonicalize trap

The instinctive choice for “turn this into a full path” is fs::canonicalize. It works — until it doesn’t:

1
2
3
4
use std::fs;

let p = fs::canonicalize("does_not_exist.toml");
assert!(p.is_err()); // canonicalize requires the path to exist

It also resolves symlinks and walks every .. component against the real directory tree. That’s the right behaviour for finding a file. It’s wrong for printing one back to the user before you’ve written it.

path::absolute does the syntactic thing

std::path::absolute joins a relative path with the current working directory and normalises the result. No syscalls beyond looking up the CWD; the file doesn’t have to exist:

1
2
3
4
5
use std::path::absolute;

let p = absolute("config/app.toml").unwrap();
assert!(p.is_absolute());
// e.g. "/work/config/app.toml" — without ever opening anything

If the path is already absolute it’s left alone (modulo platform-specific normalisation). .. components are resolved syntactically, without consulting the filesystem for what each directory really is.

Useful for nicely-formatted output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::path::{absolute, PathBuf};

fn describe(relative: &str) -> String {
    let abs: PathBuf = absolute(relative).unwrap();
    format!("writing to {}", abs.display())
}

let msg = describe("logs/today.log");
assert!(msg.contains("logs/today.log"));
assert!(msg.starts_with("writing to "));

When you’re echoing the user’s choices back to them, or building helpful error messages, this is usually what you want — the path they meant, not whatever the filesystem turned it into.

When to reach for it

Use path::absolute for log lines, config previews, default-location calculations, or any “this is where it will go” message about a file that might not exist yet. Stick with fs::canonicalize when you actually want to follow symlinks and prove the file exists — that’s its job.

Stabilised in Rust 1.79 (June 2024).

#116 May 2026

116. Path::file_prefix — Get the Real Stem of archive.tar.gz

Path::file_stem strips the last extension, so archive.tar.gz comes back as archive.tar. That’s almost never what you want for double-extension files. file_prefix strips from the first dot instead — archive, finally.

The classic confusion. You ask for the “stem” of a tarball and get something with .tar still glued on:

1
2
3
4
5
6
use std::path::Path;

let p = Path::new("backups/archive.tar.gz");

assert_eq!(p.file_stem(),   Some("archive.tar".as_ref()));
assert_eq!(p.extension(),   Some("gz".as_ref()));

file_stem takes the file name and drops everything from the last . onwards. For a single extension that’s fine. For .tar.gz, .min.js, .d.ts, .spec.ts, you end up doing the second strip yourself:

1
2
3
4
5
6
7
8
9
use std::path::Path;

fn real_stem_old(p: &Path) -> Option<&str> {
    let stem = p.file_stem()?.to_str()?;
    Some(stem.split('.').next().unwrap_or(stem))
}

assert_eq!(real_stem_old(Path::new("archive.tar.gz")), Some("archive"));
assert_eq!(real_stem_old(Path::new("bundle.min.js")),  Some("bundle"));

Works, but you’ve left OsStr land just to do a string split, and you’ve quietly made the function lossy on non-UTF-8 paths.

Rust 1.91 stabilised Path::file_prefix. It returns the file name up to the first . — staying in OsStr the whole time:

1
2
3
4
5
6
use std::path::Path;

assert_eq!(Path::new("archive.tar.gz").file_prefix(), Some("archive".as_ref()));
assert_eq!(Path::new("bundle.min.js").file_prefix(),  Some("bundle".as_ref()));
assert_eq!(Path::new("notes.md").file_prefix(),       Some("notes".as_ref()));
assert_eq!(Path::new("README").file_prefix(),         Some("README".as_ref()));

Leading dots on dotfiles are kept — exactly like file_stem already does — so you don’t accidentally turn .bashrc into an empty string:

1
2
3
4
use std::path::Path;

assert_eq!(Path::new(".bashrc").file_prefix(),     Some(".bashrc".as_ref()));
assert_eq!(Path::new(".config.toml").file_prefix(), Some(".config".as_ref()));

Pair it with file_stem when you want both halves of a multi-extension name in one place:

1
2
3
4
5
6
7
8
use std::path::Path;

let p = Path::new("logs/app.2026-05-03.log.gz");
let prefix = p.file_prefix().and_then(|s| s.to_str()).unwrap_or("");
let stem   = p.file_stem().and_then(|s| s.to_str()).unwrap_or("");

assert_eq!(prefix, "app");                     // the real name
assert_eq!(stem,   "app.2026-05-03.log");      // everything except the final ext

Reach for file_prefix whenever a filename has more than one dot and you want the part a human would call “the name”.