Sometimes, for instance when reading some configuration file, you read a file path entered by the user without going through the shell (for instance, you get ~/test
).
As Option 2
below doesn’t write to test file in user home directory, I’m wondering if there is something more idiomatic than Option 1
.
use std::env::var;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
fn write_to(path: &Path) {
let mut f = File::create(path).unwrap();
f.write_all("Hi".as_bytes()).unwrap();
}
fn main() {
// Option 1
let from_env = format!("{}/test", var("HOME").unwrap());
let with_var = Path::new(&from_env);
// Create $HOME/test
write_to(with_var);
// Option 2
let with_tilde = Path::new("~/test");
// Create the test file in current directory, provided a directory ./~ exists
write_to(with_tilde);
}
Note: unwrap()
is used here to keep the example short. There should be some error handling in production code.
The most idiomatic way would be to just use an existing crate, in this case shellexpand
(github, crates.io) seems to do what you want:
extern crate shellexpand; // 1.0.0
#[test]
fn test_shellexpand() {
let home = std::env::var("HOME").unwrap();
assert_eq!(shellexpand::tilde("~/foo"), format!("{}/foo", home));
}
Alternatively, you could try it with dirs
(crates.io). Here is a sketch:
extern crate dirs; // 1.0.4
use std::path::{Path, PathBuf};
fn expand_tilde<P: AsRef<Path>>(path_user_input: P) -> Option<PathBuf> {
let p = path_user_input.as_ref();
if !p.starts_with("~") {
return Some(p.to_path_buf());
}
if p == Path::new("~") {
return dirs::home_dir();
}
dirs::home_dir().map(|mut h| {
if h == Path::new("/") {
// Corner case: `h` root directory;
// don't prepend extra `/`, just drop the tilde.
p.strip_prefix("~").unwrap().to_path_buf()
} else {
h.push(p.strip_prefix("~/").unwrap());
h
}
})
}
Usage examples:
#[test]
fn test_expand_tilde() {
// Should work on your linux box during tests, would fail in stranger
// environments!
let home = std::env::var("HOME").unwrap();
let projects = PathBuf::from(format!("{}/Projects", home));
assert_eq!(expand_tilde("~/Projects"), Some(projects));
assert_eq!(expand_tilde("/foo/bar"), Some("/foo/bar".into()));
assert_eq!(
expand_tilde("~alice/projects"),
Some("~alice/projects".into())
);
}
Some remarks:
P: AsRef<Path>
input type imitates what the standard
library does. This is why the method accepts all Path
-like
inputs, like &str
, &OsStr
, and &Path
.Path::new
doesn't allocate anything, it points to
exactly the same bytes as the &str
.strip_prefix("~/").unwrap()
should never fail here,
because we checked that the path starts with ~
and
is not just ~
. The only way how this can be is that
the path starts with ~/
(because of how starts_with
is defined).