expand_tilde/
lib.rs

1//! Expanding tildes in paths.
2//!
3//! If the path starts with the `~` character, it will be expanded to the home directory.
4//!
5//! Any presence of `~` in paths except for the first character will not be expanded.
6//!
7//! The home directory is provided by the [`home_dir`] function.
8//!
9//! # Example
10//!
11//! There are two main ways to expand tildes with this crate;
12//! the first is to use the [`expand_tilde`] with references:
13//!
14//! ```
15//! use expand_tilde::expand_tilde;
16//!
17//! let path = "~/.config";
18//!
19//! let expanded = expand_tilde(path).unwrap();
20//!
21//! println!("{}", expanded.display());  // something like `/home/nekit/.config`
22//! ```
23//!
24//! And the other way is to use the sealed extension trait:
25//!
26//! ```
27//! use expand_tilde::ExpandTilde;
28//!
29//! let path = "~/.config";
30//!
31//! let expanded = path.expand_tilde().unwrap();
32//!
33//! println!("{}", expanded.display());  // something like `/home/nekit/.config`
34//! ```
35//!
36//! The latter method simply calls the former one under the hood.
37
38#![forbid(unsafe_code)]
39#![deny(missing_docs)]
40
41use std::{
42    borrow::Cow,
43    path::{Path, PathBuf},
44};
45
46#[cfg(feature = "diagnostics")]
47use miette::Diagnostic;
48use thiserror::Error;
49
50/// Represents errors that can occur during `~` expansion.
51///
52/// The only error that can occur is if the home directory cannot be found.
53#[derive(Debug, Error)]
54#[cfg_attr(feature = "diagnostics", derive(Diagnostic))]
55pub enum Error {
56    /// The home directory cannot be found.
57    #[error("home directory not found")]
58    #[cfg_attr(
59        feature = "diagnostics",
60        diagnostic(
61            code(expand_tilde::not_found),
62            help("make sure the home directory exists")
63        )
64    )]
65    NotFound,
66    /// The home directory is empty.
67    #[error("home directory is empty")]
68    #[cfg_attr(
69        feature = "diagnostics",
70        diagnostic(
71            code(expand_tilde::empty),
72            help("make sure the home directory is non-empty")
73        )
74    )]
75    Empty,
76}
77
78/// The result type used by this crate.
79pub type Result<T> = std::result::Result<T, Error>;
80
81/// The `~` literal.
82pub const TILDE: &str = "~";
83
84/// Wraps [`home::home_dir`] to improve diagnostics.
85///
86/// # Errors
87///
88/// Returns:
89///
90/// - [`Error::NotFound`] if the home directory cannot be found.
91/// - [`Error::Empty`] if the home directory is empty.
92pub fn home_dir() -> Result<PathBuf> {
93    let dir = home::home_dir().ok_or(Error::NotFound)?;
94
95    if dir.as_os_str().is_empty() {
96        Err(Error::Empty)
97    } else {
98        Ok(dir)
99    }
100}
101
102/// Expands the tilde (`~`) component of the given path to the home directory.
103///
104/// # Errors
105///
106/// This function propagates errors returned by [`home_dir`].
107pub fn expand_tilde<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Cow<'_, Path>> {
108    fn expand_tilde_inner(path: &Path) -> Result<Cow<'_, Path>> {
109        path.strip_prefix(TILDE).map_or_else(
110            |_| Ok(Cow::Borrowed(path)),
111            |stripped| home_dir().map(|dir| Cow::Owned(dir.join(stripped))),
112        )
113    }
114
115    expand_tilde_inner(path.as_ref())
116}
117
118/// Similar to [`expand_tilde`], but accepts the path by value and returns owned paths.
119///
120/// # Errors
121///
122/// This function propagates errors returned by [`expand_tilde`].
123pub fn expand_tilde_owned<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
124    expand_tilde(path.as_ref()).map(Cow::into_owned)
125}
126
127mod sealed {
128    pub trait Sealed {}
129}
130
131/// Represents values that can be tilde-expanded (sealed extension trait).
132pub trait ExpandTilde: sealed::Sealed {
133    /// Expands the tilde (`~`) component to the home directory.
134    ///
135    /// # Errors
136    ///
137    /// See [`expand_tilde`] for more information.
138    fn expand_tilde(&self) -> Result<Cow<'_, Path>>;
139
140    /// Similar to [`Self::expand_tilde`], but returns owned paths.
141    ///
142    /// # Errors
143    ///
144    /// See [`expand_tilde_owned`] for more information.
145    fn expand_tilde_owned(&self) -> Result<PathBuf>;
146}
147
148impl<P: AsRef<Path> + ?Sized> sealed::Sealed for P {}
149
150impl<P: AsRef<Path> + ?Sized> ExpandTilde for P {
151    fn expand_tilde(&self) -> Result<Cow<'_, Path>> {
152        expand_tilde(self)
153    }
154
155    fn expand_tilde_owned(&self) -> Result<PathBuf> {
156        expand_tilde_owned(self)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::{expand_tilde, ExpandTilde};
163
164    #[test]
165    fn consistent() {
166        let path = "~/.config";
167
168        let expanded = expand_tilde(path).unwrap();
169        let extended = path.expand_tilde().unwrap();
170
171        assert_eq!(expanded, extended);
172    }
173}