plain_path/
lib.rs

1#![deny(missing_debug_implementations, missing_docs)]
2
3//! Expands `~` in a path if present.
4//!
5//! # Examples
6//!
7//! ```rust
8//! use std::path::Path;
9//! use plain_path::PlainPathExt;
10//!
11//! # fn main() -> Result<(), plain_path::HomeDirNotFound> {
12//! let path = Path::new("~/.ssh/config").plain()?;
13//!
14//! // 🍏: "/Users/<user>/.ssh/config"
15//! // 🐧: "/home/<user>/.ssh/config"
16//! println!("{}", path.display());
17//! # Ok(())
18//! # }
19//! ```
20
21use std::{
22    fmt,
23    path::{Path, PathBuf},
24};
25
26use std::borrow::Cow;
27
28/// Provides the [`plain`][PlainPathExt::plain] method to expand `~`.
29pub trait PlainPathExt {
30    /// Returns the path without special expansion characters.
31    ///
32    /// If there are no expansion characters, the original path is returned
33    /// under the `Cow::Borrowed` variant, otherwise an owned
34    /// [`PathBuf`][std::path::PathBuf] is returned.
35    fn plain(&self) -> Result<Cow<'_, Path>, HomeDirNotFound>;
36}
37
38impl PlainPathExt for Path {
39    fn plain(&self) -> Result<Cow<'_, Path>, HomeDirNotFound> {
40        crate::plain(self)
41    }
42}
43
44impl PlainPathExt for PathBuf {
45    fn plain(&self) -> Result<Cow<'_, Path>, HomeDirNotFound> {
46        crate::plain(self)
47    }
48}
49
50/// Returns the path without special expansion characters.
51///
52/// Currently this only expands `~` to the user's home directory.
53/// Symlinks are not converted.
54pub fn plain(path: &Path) -> Result<Cow<'_, Path>, HomeDirNotFound> {
55    if path.starts_with("~") {
56        // Replace `~` with user's home directory.
57        dirs::home_dir()
58            .map(|mut path_normalized| {
59                path_normalized.extend(path.into_iter().skip(1));
60                Cow::Owned(path_normalized)
61            })
62            .ok_or(HomeDirNotFound)
63    } else {
64        Ok(Cow::Borrowed(path))
65    }
66}
67
68/// Error when the user's home directory cannot be found.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct HomeDirNotFound;
71
72impl fmt::Display for HomeDirNotFound {
73    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74        write!(f, "Failed to determine user's home directory.")
75    }
76}
77
78impl std::error::Error for HomeDirNotFound {}
79
80#[cfg(test)]
81mod tests {
82    use std::path::{Component, Path};
83
84    use super::{HomeDirNotFound, PlainPathExt};
85
86    #[test]
87    fn expands_tilde() -> Result<(), HomeDirNotFound> {
88        let path = Path::new("~/.ssh/config").plain()?;
89
90        let mut components = path.components();
91        assert_eq!(Some(Component::RootDir), components.next());
92
93        #[cfg(osx)]
94        assert_eq!(
95            Some(Component::Normal(OsStr::new("Users"))),
96            components.next()
97        );
98
99        #[cfg(linux)]
100        assert_eq!(
101            Some(Component::Normal(OsStr::new("home"))),
102            components.next()
103        );
104
105        Ok(())
106    }
107}