#266 Jul 18, 2026

266. Path::extension — Splitting on '.' Breaks on .gitignore

Grabbing a file extension with split('.') works right up until it meets .gitignore — which suddenly has the “extension” gitignore. Path::extension knows the actual rules.

The tempting one-liner:

1
2
3
4
5
let ext = "backup.tar.gz".split('.').last();
assert_eq!(ext, Some("gz")); // fine so far...

let ext = ".gitignore".split('.').last();
assert_eq!(ext, Some("gitignore")); // that's the *name*!

A dotfile’s leading dot is part of its name, not an extension separator. Your homemade splitter doesn’t know that. Path::extension and Path::file_stem do:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::ffi::OsStr;
use std::path::Path;

let p = Path::new("backup.tar.gz");
assert_eq!(p.extension(), Some(OsStr::new("gz")));
assert_eq!(p.file_stem(), Some(OsStr::new("backup.tar")));

// The dot IS the name — no extension here
let hidden = Path::new(".gitignore");
assert_eq!(hidden.extension(), None);
assert_eq!(hidden.file_stem(), Some(OsStr::new(".gitignore")));

let plain = Path::new("Makefile");
assert_eq!(plain.extension(), None);

The rules extension applies:

  • everything after the last dot, so backup.tar.gzgz (and the stem keeps backup.tar)
  • a leading dot with no other dot means dotfile, not extension: .gitignoreNone
  • no dot at all → None, instead of handing you the whole filename back

Both methods return Option<&OsStr>, so the “there is no extension” case is a None you must handle — not an empty string or a full filename silently pretending to be one.

← Previous 265. Path::strip_prefix — Compare Paths by Component, Not by Character Next → 267. Path::file_name — rsplit('/') Hands You Empty Strings and '..'