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”.