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
46use miette::Diagnostic;
47use thiserror::Error;
48
49/// Represents errors that can occur during `~` expansion.
50///
51/// The only error that can occur is if the home directory cannot be found.
52#[derive(Debug, Error, Diagnostic)]
53pub enum Error {
54    /// The home directory cannot be found.
55    #[error("home directory not found")]
56    #[diagnostic(
57        code(expand_tilde::not_found),
58        help("make sure the home directory exists")
59    )]
60    NotFound,
61    /// The home directory is empty.
62    #[error("home directory is empty")]
63    #[diagnostic(
64        code(expand_tilde::empty),
65        help("make sure the home directory is non-empty")
66    )]
67    Empty,
68}
69
70/// The result type used by this crate.
71pub type Result<T> = std::result::Result<T, Error>;
72
73/// The `~` literal.
74pub const TILDE: &str = "~";
75
76/// Wraps [`home::home_dir`] to improve diagnostics.
77///
78/// # Errors
79///
80/// Returns:
81///
82/// - [`Error::NotFound`] if the home directory cannot be found.
83/// - [`Error::Empty`] if the home directory is empty.
84pub fn home_dir() -> Result<PathBuf> {
85    let dir = home::home_dir().ok_or(Error::NotFound)?;
86
87    if dir.as_os_str().is_empty() {
88        Err(Error::Empty)
89    } else {
90        Ok(dir)
91    }
92}
93
94/// Expands the tilde (`~`) component of the given path to the home directory.
95///
96/// # Errors
97///
98/// This function propagates errors returned by [`home_dir`].
99pub fn expand_tilde<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Cow<'_, Path>> {
100    fn expand_tilde_inner(path: &Path) -> Result<Cow<'_, Path>> {
101        path.strip_prefix(TILDE).map_or_else(
102            |_| Ok(Cow::Borrowed(path)),
103            |stripped| home_dir().map(|dir| Cow::Owned(dir.join(stripped))),
104        )
105    }
106
107    expand_tilde_inner(path.as_ref())
108}
109
110/// Similar to [`expand_tilde`], but accepts the path by value and returns owned paths.
111///
112/// # Errors
113///
114/// This function propagates errors returned by [`expand_tilde`].
115pub fn expand_tilde_owned<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
116    expand_tilde(path.as_ref()).map(Cow::into_owned)
117}
118
119mod sealed {
120    pub trait Sealed {}
121}
122
123/// Represents values that can be tilde-expanded (sealed extension trait).
124pub trait ExpandTilde: sealed::Sealed {
125    /// Expands the tilde (`~`) component to the home directory.
126    ///
127    /// # Errors
128    ///
129    /// See [`expand_tilde`] for more information.
130    fn expand_tilde(&self) -> Result<Cow<'_, Path>>;
131}
132
133impl<P: AsRef<Path>> sealed::Sealed for P {}
134
135impl<P: AsRef<Path>> ExpandTilde for P {
136    fn expand_tilde(&self) -> Result<Cow<'_, Path>> {
137        expand_tilde(self)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::{expand_tilde, ExpandTilde};
144
145    #[test]
146    fn consistent() {
147        let path = "~/.config";
148
149        let expanded = expand_tilde(path).unwrap();
150        let extended = path.expand_tilde().unwrap();
151
152        assert_eq!(expanded, extended);
153    }
154}