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}