use anyhow::{bail, Result}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; pub use snailquote::escape; use std::path::{Component, Path, PathBuf}; lazy_static! { pub static ref HOME: Option = dirs::home_dir(); } // Stolen from https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs pub fn diff(path: P, base: B) -> Result where P: AsRef, B: AsRef, { let path = path.as_ref(); let base = base.as_ref(); if path.is_absolute() != base.is_absolute() { if path.is_absolute() { Ok(PathBuf::from(path)) } else { let path = path.to_string_lossy(); bail!("{path}: is not absolute") } } else { let mut ita = path.components(); let mut itb = base.components(); let mut comps: Vec = vec![]; loop { match (ita.next(), itb.next()) { (None, None) => break, (Some(a), None) => { comps.push(a); comps.extend(ita.by_ref()); break; } (None, _) => comps.push(Component::ParentDir), (Some(a), Some(b)) if comps.is_empty() && a == b => (), (Some(a), Some(b)) if b == Component::CurDir => comps.push(a), (Some(_), Some(b)) if b == Component::ParentDir => { let path = path.to_string_lossy(); let base = base.to_string_lossy(); bail!("{base} is not a parent of {path}") } (Some(a), Some(_)) => { comps.push(Component::ParentDir); for _ in itb { comps.push(Component::ParentDir); } comps.push(a); comps.extend(ita.by_ref()); break; } } } Ok(comps.iter().map(|c| c.as_os_str()).collect()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct RelativityConfig> { base: Option, with_prefix_dots: Option, without_suffix_dots: Option, } impl> RelativityConfig { pub fn with_base(mut self, base: B) -> Self { self.base = Some(base); self } pub fn with_prefix_dots(mut self) -> Self { self.with_prefix_dots = Some(true); self } pub fn without_suffix_dots(mut self) -> Self { self.without_suffix_dots = Some(true); self } } pub fn relative_to( path: P, config: Option<&RelativityConfig>, ) -> Result where P: AsRef, B: AsRef, { let path = path.as_ref(); let base = match config.and_then(|c| c.base.as_ref()) { Some(base) => PathBuf::from(base.as_ref()), None => std::env::current_dir()?, }; let diff = diff(path, base)?; let relative = if diff.to_str() == Some("") { ".".into() } else { diff }; let relative = if config.and_then(|c| c.with_prefix_dots).unwrap_or(false) && !relative.starts_with(".") && !relative.starts_with("..") { PathBuf::from(".").join(relative) } else { relative }; let relative = if !config.and_then(|c| c.without_suffix_dots).unwrap_or(false) { relative } else if relative.ends_with(".") { match (path.parent(), path.file_name()) { (Some(_), Some(name)) => PathBuf::from("..").join(name), (_, _) => relative, } } else if relative.ends_with("..") { match (path.parent(), path.file_name()) { (Some(parent), Some(name)) => { if parent.parent().is_some() { relative.join("..").join(name) } else { // always prefer absolute path if it's a child of the root directory // to guarantee that the basename is included path.into() } } (_, _) => relative, } } else { relative }; Ok(relative) } pub fn shorten(path: P, config: Option<&RelativityConfig>) -> Result where P: AsRef, B: AsRef, { let path = path.as_ref(); let pathstring = path.to_string_lossy().to_string(); let relative = relative_to(path, config)?; let relative = relative.to_string_lossy().to_string(); let fromhome = HOME .as_ref() .and_then(|h| { path.strip_prefix(h).ok().map(|p| { if p.to_str() == Some("") { "~".into() } else { PathBuf::from("~").join(p).to_string_lossy().to_string() } }) }) .unwrap_or(pathstring); if relative.len() < fromhome.len() { Ok(relative) } else { Ok(fromhome) } } #[cfg(test)] mod tests { use super::*; type Config<'a> = Option<&'a RelativityConfig>; const NONE: Config = Config::None; fn default<'a>() -> RelativityConfig<&'a str> { Default::default() } #[test] fn test_relative_to_pwd() { let path = std::env::current_dir().unwrap(); let relative = relative_to(&path, NONE).unwrap(); assert_eq!(relative, PathBuf::from(".")); let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap(); assert_eq!(relative, PathBuf::from(".")); let relative = relative_to(&path, Some(&default().without_suffix_dots())).unwrap(); assert_eq!( relative, PathBuf::from("..").join(path.file_name().unwrap()) ); let relative = relative_to( &path, Some(&default().with_prefix_dots().without_suffix_dots()), ) .unwrap(); assert_eq!( relative, PathBuf::from("..").join(path.file_name().unwrap()) ); } #[test] fn test_relative_to_parent() { let path = std::env::current_dir().unwrap(); let parent = path.parent().unwrap(); let relative = relative_to(parent, NONE).unwrap(); assert_eq!(relative, PathBuf::from("..")); let relative = relative_to(parent, Some(&default().with_prefix_dots())).unwrap(); assert_eq!(relative, PathBuf::from("..")); let relative = relative_to(parent, Some(&default().without_suffix_dots())).unwrap(); assert_eq!( relative, PathBuf::from("../..").join(parent.file_name().unwrap()) ); let relative = relative_to( parent, Some(&default().with_prefix_dots().without_suffix_dots()), ) .unwrap(); assert_eq!( relative, PathBuf::from("../..").join(parent.file_name().unwrap()) ); } #[test] fn test_relative_to_file() { let path = std::env::current_dir().unwrap().join("foo").join("bar"); let relative = relative_to(&path, NONE).unwrap(); assert_eq!(relative, PathBuf::from("foo/bar")); let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap(); assert_eq!(relative, PathBuf::from("./foo/bar")); let relative = relative_to( &path, Some(&default().with_prefix_dots().without_suffix_dots()), ) .unwrap(); assert_eq!(relative, PathBuf::from("./foo/bar")); } #[test] fn test_relative_to_root() { let relative = relative_to("/foo", Some(&default().with_base("/"))).unwrap(); assert_eq!(relative, PathBuf::from("foo")); let relative = relative_to( "/foo", Some( &default() .with_base("/") .with_prefix_dots() .without_suffix_dots(), ), ) .unwrap(); assert_eq!(relative, PathBuf::from("./foo")); let relative = relative_to("/", Some(&default().with_base("/"))).unwrap(); assert_eq!(relative, PathBuf::from(".")); let relative = relative_to( "/", Some( &default() .with_base("/") .with_prefix_dots() .without_suffix_dots(), ), ) .unwrap(); assert_eq!(relative, PathBuf::from(".")); let relative = relative_to("/", Some(&default().with_base("/foo"))).unwrap(); assert_eq!(relative, PathBuf::from("..")); let relative = relative_to( "/", Some( &default() .with_base("/foo") .with_prefix_dots() .without_suffix_dots(), ), ) .unwrap(); assert_eq!(relative, PathBuf::from("..")); } #[test] fn test_relative_to_base() { let path = "/some/directory"; let base = "/another/foo/bar"; let relative = relative_to(path, Some(&default().with_base(base))).unwrap(); assert_eq!(relative, PathBuf::from("../../../some/directory")); let relative = relative_to( path, Some( &default() .with_base(base) .with_prefix_dots() .without_suffix_dots(), ), ) .unwrap(); assert_eq!(relative, PathBuf::from("../../../some/directory")); } #[test] fn test_shorten_home() { let path = HOME.as_ref().unwrap(); let res = shorten(path, NONE).unwrap(); assert_eq!(res, "~"); let res = shorten( path, Some(&default().with_prefix_dots().without_suffix_dots()), ) .unwrap(); assert_eq!(res, "~"); let res = shorten( path, Some(&default().with_prefix_dots().without_suffix_dots()), ) .unwrap(); assert_eq!(res, "~"); let res = shorten(path.join("foo"), NONE).unwrap(); assert_eq!(res, "~/foo"); let res = shorten( path.join("foo"), Some(&default().with_prefix_dots().without_suffix_dots()), ) .unwrap(); assert_eq!(res, "~/foo"); let res = shorten(format!("{}foo", path.to_string_lossy()), NONE).unwrap(); assert_ne!(res, "~/foo"); assert_eq!(res, format!("{}foo", path.to_string_lossy())); } #[test] fn test_shorten_base() { let path = "/present/working/directory"; let base = "/present/foo/bar"; let res = shorten(path, Some(&default().with_base(base))).unwrap(); assert_eq!(res, "../../working/directory"); let res = shorten( path, Some( &default() .with_base(base) .with_prefix_dots() .without_suffix_dots(), ), ) .unwrap(); assert_eq!(res, "../../working/directory"); } #[test] fn test_shorten_pwd() { let path = "/present/working/directory"; let res = shorten(path, Some(&default().with_base(path))).unwrap(); assert_eq!(res, "."); let res = shorten( path, Some( &default() .with_base(path) .with_prefix_dots() .without_suffix_dots(), ), ) .unwrap(); assert_eq!(res, "../directory"); } #[test] fn test_shorten_parent() { let path = "/present/working"; let base = "/present/working/directory"; let res = shorten(path, Some(&default().with_base(base))).unwrap(); assert_eq!(res, ".."); let res = shorten( path, Some( &default() .with_base(base) .with_prefix_dots() .without_suffix_dots(), ), ) .unwrap(); assert_eq!(res, "../../working"); } #[test] fn test_shorten_root() { let res = shorten("/", Some(&default().with_base("/"))).unwrap(); assert_eq!(res, "/"); let res = shorten( "/", Some( &default() .with_base("/") .with_prefix_dots() .without_suffix_dots(), ), ) .unwrap(); assert_eq!(res, "/"); let res = shorten("/foo", Some(&default().with_base("/"))).unwrap(); assert_eq!(res, "foo"); let res = shorten( "/foo", Some( &default() .with_base("/") .with_prefix_dots() .without_suffix_dots(), ), ) .unwrap(); assert_eq!(res, "/foo"); let res = shorten( "/", Some( &default() .with_base("/foo") .with_prefix_dots() .without_suffix_dots(), ), ) .unwrap(); assert_eq!(res, "/"); } #[test] fn test_path_escape() { let text = "foo".to_string(); assert_eq!(escape(&text), "foo"); let text = "foo bar".to_string(); assert_eq!(escape(&text), "'foo bar'"); let text = "foo\nbar".to_string(); assert_eq!(escape(&text), "\"foo\\nbar\""); let text = "foo$bar".to_string(); assert_eq!(escape(&text), "'foo$bar'"); let text = "foo'$\n'bar".to_string(); assert_eq!(escape(&text), "\"foo'\\$\\n'bar\""); let text = "a'b\"c".to_string(); assert_eq!(escape(&text), "\"a'b\\\"c\""); } }