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}