Path

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