Reputation: 983
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.
Upvotes: 34
Views: 13286
Reputation: 492
EDIT:
The expanduser
crate probably does everything you want, including expansion of ~user
. An example (adapted from the documentation):
use expanduser::expanduser;
let path = expanduser("~foolmeonce/path/to/directory")?;
assert_eq!(path.display().to_string(), "/home/foolmeonce/path/to/directory");
let path = expanduser("~/path/to/directory")?;
assert_eq!(path.display().to_string(), "/home/foolmetwice/path/to/directory");
ORIGINAL ANSWER:
Here's an implementation returning a Cow<Path>
, so that we only allocate if there's actually a tilde prefix in the path:
use std::{borrow::Cow, path::Path};
use directories::UserDirs;
use lazy_static::lazy_static;
fn expand_home_dir<'a, P: AsRef<Path> + ?Sized>(path: &'a P) -> Cow<'a, Path> {
let path = path.as_ref();
if !path.starts_with("~") {
return path.into();
}
lazy_static! {
static ref HOME_DIR: &'static Path = UserDirs::new().unwrap().home_dir();
}
HOME_DIR.join(path.strip_prefix("~").unwrap()).into()
}
Things to notice:
unwrap
that could fail is the one in the lazy_static!
block, but there's no recovery from it.join
.Some usage examples:
#[test]
fn test_expand_home_dir() {
lazy_static! {
static ref HOME_DIR: String = std::env::var("HOME").unwrap();
}
// Simple prefix expansion.
assert_eq!(
expand_home_dir("~/a/path/to/a/file"),
Path::new(&format!("{}/a/path/to/a/file", &*HOME_DIR))
);
// Lone tilde is user's home directory.
assert_eq!(expand_home_dir("~"), Path::new(&*HOME_DIR));
// Tilde in the middle of a path should not be expanded.
assert_eq!(
expand_home_dir("/a/~/path/to/a/file"),
Path::new("/a/~/path/to/a/file")
);
// No tilde, no expansion in absolute paths.
assert_eq!(
expand_home_dir("/a/path/to/a/file"),
Path::new("/a/path/to/a/file")
);
// No tilde, no expansion in relative paths.
assert_eq!(
expand_home_dir("another/path/to/a/file"),
Path::new("another/path/to/a/file")
);
}
Upvotes: 5
Reputation: 44918
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).Upvotes: 41