expand_tilde/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//! Expanding tildes in paths.
//!
//! If the path starts with the `~` character, it will be expanded to the home directory.
//!
//! Any presence of `~` in paths except for the first character will not be expanded.
//!
//! The home directory is provided by the [`home_dir`] function.
//!
//! # Example
//!
//! There are two main ways to expand tildes with this crate;
//! the first is to use the [`expand_tilde`] function directly:
//!
//! ```
//! use expand_tilde::expand_tilde;
//!
//! let path = "~/.config";
//!
//! let expanded = expand_tilde(path).unwrap();
//!
//! println!("{}", expanded.display());  // something like `/home/nekit/.config`
//! ```
//!
//! And the other way is to use the sealed extension trait:
//!
//! ```
//! use expand_tilde::ExpandTilde;
//!
//! let path = "~/.config";
//!
//! let expanded = path.expand_tilde().unwrap();
//!
//! println!("{}", expanded.display());  // something like `/home/nekit/.config`
//! ```
//!
//! The latter method simply calls the former one under the hood.

#![forbid(unsafe_code)]
#![deny(missing_docs)]

use std::{
    borrow::Cow,
    path::{Path, PathBuf},
};

use miette::Diagnostic;
use thiserror::Error;

/// Represents errors that can occur during `~` expansion.
///
/// The only error that can occur is if the home directory cannot be found.
#[derive(Debug, Error, Diagnostic)]
pub enum Error {
    /// The home directory cannot be found.
    #[error("home directory not found")]
    #[diagnostic(
        code(expand_tilde::not_found),
        help("make sure the home directory exists")
    )]
    NotFound,
    /// The home directory is empty.
    #[error("home directory is empty")]
    #[diagnostic(
        code(expand_tilde::empty),
        help("make sure the home directory is non-empty")
    )]
    Empty,
}

/// The result type used by this crate.
pub type Result<T> = std::result::Result<T, Error>;

/// The `~` literal.
pub const TILDE: &str = "~";

/// Wraps [`home::home_dir`] to improve diagnostics.
///
/// # Errors
///
/// Returns:
///
/// - [`Error::NotFound`] if the home directory cannot be found.
/// - [`Error::Empty`] if the home directory is empty.
pub fn home_dir() -> Result<PathBuf> {
    let dir = home::home_dir().ok_or(Error::NotFound)?;

    if dir.as_os_str().is_empty() {
        Err(Error::Empty)
    } else {
        Ok(dir)
    }
}

/// Expands the tilde (`~`) component to the home directory.
///
/// This function is similar to [`expand_tilde_path`], except it is generic over the path type.
///
/// # Errors
///
/// See [`expand_tilde_path`] for more information.
pub fn expand_tilde<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Cow<'_, Path>> {
    expand_tilde_path(path.as_ref())
}

/// Expands the tilde (`~`) component of the [`Path`] to the home directory.
///
/// # Errors
///
/// Returns:
///
/// - [`Error::NotFound`] if the home directory cannot be found.
/// - [`Error::Empty`] if the home directory is empty.
pub fn expand_tilde_path(path: &Path) -> Result<Cow<'_, Path>> {
    path.strip_prefix(TILDE).map_or_else(
        |_| Ok(Cow::Borrowed(path)),
        |stripped| home_dir().map(|dir| Cow::Owned(dir.join(stripped))),
    )
}

mod private {
    pub trait Sealed {}
}

/// Represents values that can be tilde-expanded (sealed extension trait).
pub trait ExpandTilde: private::Sealed {
    /// Expands the tilde (`~`) component to the home directory.
    ///
    /// # Errors
    ///
    /// See [`expand_tilde_path`] for more information.
    fn expand_tilde(&self) -> Result<Cow<'_, Path>>;
}

impl<P: AsRef<Path>> private::Sealed for P {}

impl<P: AsRef<Path>> ExpandTilde for P {
    fn expand_tilde(&self) -> Result<Cow<'_, Path>> {
        expand_tilde(self)
    }
}

#[cfg(test)]
mod tests {
    use super::{expand_tilde, ExpandTilde};

    #[test]
    fn consistent() {
        let path = "~/.config";

        let expanded = expand_tilde(path).unwrap();
        let extended = path.expand_tilde().unwrap();

        assert_eq!(expanded, extended);
    }
}