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
//! 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`] with references:
//!
//! ```
//! 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 of the given path to the home directory.
///
/// # Errors
///
/// This function propagates errors returned by [`home_dir`].
pub fn expand_tilde<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Cow<'_, Path>> {
fn expand_tilde_inner(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))),
)
}
expand_tilde_inner(path.as_ref())
}
mod sealed {
pub trait Sealed {}
}
/// Represents values that can be tilde-expanded (sealed extension trait).
pub trait ExpandTilde: sealed::Sealed {
/// Expands the tilde (`~`) component to the home directory.
///
/// # Errors
///
/// See [`expand_tilde`] for more information.
fn expand_tilde(&self) -> Result<Cow<'_, Path>>;
}
impl<P: AsRef<Path>> sealed::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);
}
}