|
|
|
use crate::dirs;
|
|
|
|
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<PathBuf> = dirs::home_dir();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stolen from https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs
|
|
|
|
pub fn diff<P, B>(path: P, base: B) -> Result<PathBuf>
|
|
|
|
where
|
|
|
|
P: AsRef<Path>,
|
|
|
|
B: AsRef<Path>,
|
|
|
|
{
|
|
|
|
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<Component> = 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<B: AsRef<Path>> {
|
|
|
|
base: Option<B>,
|
|
|
|
with_prefix_dots: Option<bool>,
|
|
|
|
without_suffix_dots: Option<bool>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<B: AsRef<Path>> RelativityConfig<B> {
|
|
|
|
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<P, B>(
|
|
|
|
path: P,
|
|
|
|
config: Option<&RelativityConfig<B>>,
|
|
|
|
) -> Result<PathBuf>
|
|
|
|
where
|
|
|
|
P: AsRef<Path>,
|
|
|
|
B: AsRef<Path>,
|
|
|
|
{
|
|
|
|
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<P, B>(path: P, config: Option<&RelativityConfig<B>>) -> Result<String>
|
|
|
|
where
|
|
|
|
P: AsRef<Path>,
|
|
|
|
B: AsRef<Path>,
|
|
|
|
{
|
|
|
|
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<String>>;
|
|
|
|
|
|
|
|
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().join("docs");
|
|
|
|
let parent = path.parent().unwrap();
|
|
|
|
|
|
|
|
let base = default().with_base(path.to_str().unwrap());
|
|
|
|
|
|
|
|
let relative = relative_to(parent, Some(&base)).unwrap();
|
|
|
|
assert_eq!(relative, PathBuf::from(".."));
|
|
|
|
|
|
|
|
let relative =
|
|
|
|
relative_to(parent, Some(&base.clone().with_prefix_dots())).unwrap();
|
|
|
|
assert_eq!(relative, PathBuf::from(".."));
|
|
|
|
|
|
|
|
let relative =
|
|
|
|
relative_to(parent, Some(&base.clone().without_suffix_dots())).unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
relative,
|
|
|
|
PathBuf::from("../..").join(parent.file_name().unwrap())
|
|
|
|
);
|
|
|
|
|
|
|
|
let relative = relative_to(
|
|
|
|
parent,
|
|
|
|
Some(&base.clone().with_prefix_dots().without_suffix_dots()),
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
relative,
|
|
|
|
PathBuf::from("../..").join(parent.clone().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\"");
|
|
|
|
}
|
|
|
|
}
|