Handling paths in Rust
2021-06-21
Less easy than it looks
Rust is a very interesting language for system utility creation, a field where file & path manipulation is quite a basic & expected feature, especially in modern programming
In this post, we’ll explore rust’s path manipulation facilities.
First stage: discovery & denial🔗
The documentation tells us that std::path is what we should use.
From the docs, here’s an example:
use std::path::Path;
use std::ffi::OsStr;
let path = Path::new("/tmp/foo/bar.txt");
let parent = path.parent();
assert_eq!(parent, Some(Path::new("/tmp/foo")));
let file_stem = path.file_stem();
assert_eq!(file_stem, Some(OsStr::new("bar")));
let extension = path.extension();
assert_eq!(extension, Some(OsStr::new("txt")));
Indeed, using a rust Path is convenient: non need to call fs::metadata(&str) to get the files’ metadata!
Things get complicated when we start to need to display this Path, as would be useful in the case of application logs.
Here’s some code, written on rust playground:
fn main() {
use std::path::Path;
let path: &Path = Path::new("/tmp/foo/bar.txt");
println!("je suis le path {}!", path);
}
This code can’t be compiled, tells us the compiler, with the following message:
Compiling playground v0.0.1 (/playground)
error[E0277]: `Path` doesn't implement `std::fmt::Display`
--> src/lib.rs:5:37
|
5 | println!("je suis le path {}!", path);
| ^^^^ `Path` cannot be formatted with the default formatter.
|
= help: the trait `std::fmt::Display` is not implemented for `Path`
= note: required by `std::fmt::Display::fmt`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error
Indeed, if &Path doesn’t implement Display, we won’t be able to use it as-is.
Stage 2: negociation🔗
Maybe we could convert it to &str? The to_str(&self) method returns an Option<&str>. Not ideal, but we could try to expect or unwrap it:
fn main() {
use std::path::Path;
let path = Path::new("/tmp/foo/bar.txt");
println!("i am path {}!", path.to_str().expect("got None on path.to_str()"));
}
Compiling playground v0.0.1 (/playground)
Finished dev [unoptimized + debuginfo] target(s) in 1.76s
It works! Great! But why does &Path::to_str() return an Option ?
From the docs:
pub fn to_str(&self) -> Option<&str>Yields a
&strslice if thePathis valid unicode.This conversion may entail doing a check for UTF-8 validity. Note that validation is performed because non-UTF-8 strings are perfectly valid for some OS, like Windows.
Oh. So, if i want to make this code run on windows, I will panic! each time i’ll try to print a &Path. Very much uncool.
Stage 3: Anger🔗
While reading &Path and PathBuf’s documentation, we quickly realise they are a thinly veiled wrapper around OsStr, an abstraction around string representation on the platform.
What purpose is there to use &Path if we must implement a complex error handling logic on some simple paths?! Would be not be bettor off to simply use &str and write path manipulation functions according to the target arch?! Isn’t it what is supposed to accomplish std::path?!
Moreover, let’s consider the following snippet:
let p: &Path = Path::new("/tmp");
let s: &str = p.join("myfile.txt").to_str().expect("fuck windows");
Stage 4: Depression🔗
Why would so simple & common operations be so complicated in a modern language?
Why wouldn’t the standart library offer convenient conversion methods?
A precise and correct computer science as promises rust is not compatible with ease of use.
There is no hope, no futur, and we must accept our lot to reinvent the wheel at each new cycle of creation and suffer in silence, as Sisyphus and its boulder.
Stage 5: Acceptance🔗
Struct
std::path::Display🔗
pub struct Display<'a> { /* fields omitted */ }Helper struct for safely printing paths with
format!and{}.A
Pathmight contain non-Unicode data. This struct implements theDisplaytrait in a way that mitigates that. It is created by the display method onPath. This may perform lossy conversion, depending on the platform. If you would like an implementation which escapes the path please useDebuginstead.Examples🔗
use std::path::Path; let path = Path::new("/tmp/foo.rs"); println!("{}", path.display());
fn main() {
use std::path::Path;
let path = Path::new("/tmp/foo/bar.txt");
println!("I am path {}!", path.display());
let s: String = path.display().to_string();
println!("I am string {}!", s);
}
Compiling playground v0.0.1 (/playground)
Finished dev [unoptimized + debuginfo] target(s) in 2.05s
Running `target/debug/playground`
I am path /tmp/foo/bar.txt!
I am string /tmp/foo/bar.txt!
Oh.
tip
subjects: [ dev | rust | computer science ]