Remove deleted entries if not accessed in the last 90 days (#213)

This commit is contained in:
Ajeet D'Souza 2021-05-17 21:46:42 +05:30 committed by GitHub
parent 98f1d7277a
commit 22a9f6c32d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 637 additions and 607 deletions

64
Cargo.lock generated
View File

@ -335,12 +335,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]]
name = "ordered-float"
version = "2.2.0"
@ -356,6 +350,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
[[package]]
name = "pest"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
dependencies = [
"ucd-trie",
]
[[package]]
name = "ppv-lite86"
version = "0.2.10"
@ -554,6 +557,28 @@ dependencies = [
"winapi",
]
[[package]]
name = "rstest"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "041bb0202c14f6a158bbbf086afb03d0c6e975c2dec7d4912f8061ed44f290af"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"rustc_version",
"syn",
]
[[package]]
name = "rustc_version"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.5"
@ -561,10 +586,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "seq-macro"
version = "0.2.2"
name = "semver"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [
"pest",
]
[[package]]
name = "serde"
@ -653,6 +690,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
[[package]]
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
@ -754,10 +797,9 @@ dependencies = [
"dirs-next",
"dunce",
"glob",
"once_cell",
"ordered-float",
"rand 0.7.3",
"seq-macro",
"rstest",
"serde",
"tempfile",
]

View File

@ -26,8 +26,7 @@ rand = "0.7.3"
[dev-dependencies]
assert_cmd = "1.0.1"
once_cell = "1.4.1"
seq-macro = "0.2.1"
rstest = "0.10.0"
[build-dependencies]
clap = "3.0.0-beta.2"

View File

@ -8,6 +8,7 @@ zoxide is a smarter replacement for your cd command. It keeps track of the
directories you use most frequently, and uses a ranking algorithm to navigate
to the best match.
.SH USAGE
.nf
\fBz\fR \fIfoo\fR # cd into highest ranked directory matching foo
\fBz\fR \fIfoo bar\fR # cd into highest ranked directory matching foo and bar
.sp
@ -17,6 +18,7 @@ to the best match.
\fBz\fR \fI-\fR # cd into previous directory
.sp
\fBzi\fR \fIfoo\fR # cd with interactive selection (using fzf)
.fi
.SH SUBCOMMANDS
.TP
\fBzoxide-add\fR(1)

7
rustfmt.toml Normal file
View File

@ -0,0 +1,7 @@
# group_imports = "StdExternalCrate"
# imports_granularity = "Module"
newline_style = "Native"
use_field_init_shorthand = true
use_small_heuristics = "Max"
use_try_shorthand = true
# wrap_comments = true

View File

@ -116,7 +116,7 @@ pub struct Query {
pub score: bool,
/// Exclude a path from results
#[clap(long, hidden = true)]
#[clap(long, value_name = "path")]
pub exclude: Option<String>,
}
@ -126,9 +126,6 @@ pub struct Remove {
// Use interactive selection
#[clap(conflicts_with = "path", long, short, value_name = "keywords")]
pub interactive: Option<Vec<String>>,
#[clap(
conflicts_with = "interactive",
required_unless_present = "interactive"
)]
#[clap(conflicts_with = "interactive", required_unless_present = "interactive")]
pub path: Option<String>,
}

View File

@ -13,10 +13,7 @@ impl Run for Add {
util::resolve_path(&self.path)
}?;
if config::zo_exclude_dirs()?
.iter()
.any(|pattern| pattern.matches_path(&path))
{
if config::zo_exclude_dirs()?.iter().any(|pattern| pattern.matches_path(&path)) {
return Ok(());
}

View File

@ -31,11 +31,8 @@ fn from_autojump<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
let buffer = fs::read_to_string(path)
.with_context(|| format!("could not open autojump database: {}", path.display()))?;
let mut dirs = db
.dirs
.iter()
.map(|dir| (dir.path.as_ref(), dir.clone()))
.collect::<HashMap<_, _>>();
let mut dirs =
db.dirs.iter().map(|dir| (dir.path.as_ref(), dir.clone())).collect::<HashMap<_, _>>();
for line in buffer.lines() {
if line.is_empty() {
@ -43,28 +40,20 @@ fn from_autojump<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
}
let mut split = line.splitn(2, '\t');
let rank = split
.next()
.with_context(|| format!("invalid entry: {}", line))?;
let mut rank = rank
.parse::<f64>()
.with_context(|| format!("invalid rank: {}", rank))?;
let rank = split.next().with_context(|| format!("invalid entry: {}", line))?;
let mut rank = rank.parse::<f64>().with_context(|| format!("invalid rank: {}", rank))?;
// Normalize the rank using a sigmoid function. Don't import actual
// ranks from autojump, since its scoring algorithm is very different,
// and might take a while to get normalized.
rank = 1.0 / (1.0 + (-rank).exp());
let path = split
.next()
.with_context(|| format!("invalid entry: {}", line))?;
let path = split.next().with_context(|| format!("invalid entry: {}", line))?;
dirs.entry(path)
.and_modify(|dir| dir.rank += rank)
.or_insert_with(|| Dir {
path: path.to_string().into(),
rank,
last_accessed: 0,
});
dirs.entry(path).and_modify(|dir| dir.rank += rank).or_insert_with(|| Dir {
path: path.to_string().into(),
rank,
last_accessed: 0,
});
}
db.dirs = DirList(dirs.into_iter().map(|(_, dir)| dir).collect());
@ -78,11 +67,8 @@ fn from_z<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
let buffer = fs::read_to_string(path)
.with_context(|| format!("could not open z database: {}", path.display()))?;
let mut dirs = db
.dirs
.iter()
.map(|dir| (dir.path.as_ref(), dir.clone()))
.collect::<HashMap<_, _>>();
let mut dirs =
db.dirs.iter().map(|dir| (dir.path.as_ref(), dir.clone())).collect::<HashMap<_, _>>();
for line in buffer.lines() {
if line.is_empty() {
@ -90,23 +76,14 @@ fn from_z<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
}
let mut split = line.rsplitn(3, '|');
let last_accessed = split
.next()
.with_context(|| format!("invalid entry: {}", line))?;
let last_accessed = last_accessed
.parse()
.with_context(|| format!("invalid epoch: {}", last_accessed))?;
let last_accessed = split.next().with_context(|| format!("invalid entry: {}", line))?;
let last_accessed =
last_accessed.parse().with_context(|| format!("invalid epoch: {}", last_accessed))?;
let rank = split
.next()
.with_context(|| format!("invalid entry: {}", line))?;
let rank = rank
.parse()
.with_context(|| format!("invalid rank: {}", rank))?;
let rank = split.next().with_context(|| format!("invalid entry: {}", line))?;
let rank = rank.parse().with_context(|| format!("invalid rank: {}", rank))?;
let path = split
.next()
.with_context(|| format!("invalid entry: {}", line))?;
let path = split.next().with_context(|| format!("invalid entry: {}", line))?;
dirs.entry(path)
.and_modify(|dir| {
@ -115,11 +92,7 @@ fn from_z<P: AsRef<Path>>(db: &mut Database, path: P) -> Result<()> {
dir.last_accessed = last_accessed;
}
})
.or_insert(Dir {
path: path.to_string().into(),
rank,
last_accessed,
});
.or_insert(Dir { path: path.to_string().into(), rank, last_accessed });
}
db.dirs = DirList(dirs.into_iter().map(|(_, dir)| dir).collect());

View File

@ -10,21 +10,12 @@ use std::io::{self, Write};
impl Run for Init {
fn run(&self) -> Result<()> {
let cmd = if self.no_aliases {
None
} else {
Some(self.cmd.as_str())
};
let cmd = if self.no_aliases { None } else { Some(self.cmd.as_str()) };
let echo = config::zo_echo();
let resolve_symlinks = config::zo_resolve_symlinks();
let opts = &Opts {
cmd,
hook: self.hook,
echo,
resolve_symlinks,
};
let opts = &Opts { cmd, hook: self.hook, echo, resolve_symlinks };
let source = match self.shell {
InitShell::Bash => shell::Bash(opts).render(),

View File

@ -1,6 +1,6 @@
use crate::app::{Query, Run};
use crate::config;
use crate::db::{DatabaseFile, Matcher};
use crate::db::DatabaseFile;
use crate::error::BrokenPipeHandler;
use crate::fzf::Fzf;
use crate::util;
@ -16,19 +16,18 @@ impl Run for Query {
let mut db = db.open()?;
let now = util::current_time()?;
let mut matcher = Matcher::new().with_keywords(&self.keywords);
let mut stream = db.stream(now).with_keywords(&self.keywords);
if !self.all {
let resolve_symlinks = config::zo_resolve_symlinks();
matcher = matcher.with_exists(resolve_symlinks);
stream = stream.with_exists(resolve_symlinks);
}
if let Some(path) = &self.exclude {
stream = stream.with_exclude(path);
}
let mut matches = db
.iter(&matcher, now)
.filter(|dir| Some(dir.path.as_ref()) != self.exclude.as_deref());
if self.interactive {
let mut fzf = Fzf::new(false)?;
for dir in matches {
while let Some(dir) = stream.next() {
writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?;
}
@ -36,9 +35,7 @@ impl Run for Query {
if self.score {
print!("{}", selection);
} else {
let path = selection
.get(5..)
.context("could not read selection from fzf")?;
let path = selection.get(5..).context("could not read selection from fzf")?;
print!("{}", path)
}
} else if self.list {
@ -49,7 +46,7 @@ impl Run for Query {
let stdout = stdout.lock();
let mut handle = BufWriter::new(stdout);
for dir in matches {
while let Some(dir) = stream.next() {
if self.score {
writeln!(handle, "{}", dir.display_score(now))
} else {
@ -59,7 +56,7 @@ impl Run for Query {
}
handle.flush().pipe_exit("stdout")?;
} else {
let dir = matches.next().context("no match found")?;
let dir = stream.next().context("no match found")?;
if self.score {
writeln!(io::stdout(), "{}", dir.display_score(now))
} else {

View File

@ -1,6 +1,6 @@
use crate::app::{Remove, Run};
use crate::config;
use crate::db::{DatabaseFile, Matcher};
use crate::db::DatabaseFile;
use crate::error::BrokenPipeHandler;
use crate::fzf::Fzf;
use crate::util;
@ -18,11 +18,11 @@ impl Run for Remove {
let selection;
match &self.interactive {
Some(keywords) => {
let matcher = Matcher::new().with_keywords(keywords);
let now = util::current_time()?;
let mut stream = db.stream(now).with_keywords(keywords);
let mut fzf = Fzf::new(true)?;
for dir in db.iter(&matcher, now) {
while let Some(dir) = stream.next() {
writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?;
}

View File

@ -34,9 +34,7 @@ pub fn zo_exclude_dirs() -> Result<Vec<Pattern>> {
match env::var_os("_ZO_EXCLUDE_DIRS") {
Some(dirs_osstr) => env::split_paths(&dirs_osstr)
.map(|path| {
let pattern = path
.to_str()
.context("invalid unicode in _ZO_EXCLUDE_DIRS")?;
let pattern = path.to_str().context("invalid unicode in _ZO_EXCLUDE_DIRS")?;
Pattern::new(&pattern)
.with_context(|| format!("invalid glob in _ZO_EXCLUDE_DIRS: {}", pattern))
})
@ -61,9 +59,7 @@ pub fn zo_fzf_opts() -> Option<OsString> {
pub fn zo_maxage() -> Result<Rank> {
match env::var_os("_ZO_MAXAGE") {
Some(maxage_osstr) => {
let maxage_str = maxage_osstr
.to_str()
.context("invalid unicode in _ZO_MAXAGE")?;
let maxage_str = maxage_osstr.to_str().context("invalid unicode in _ZO_MAXAGE")?;
let maxage = maxage_str.parse::<u64>().with_context(|| {
format!("unable to parse _ZO_MAXAGE as integer: {}", maxage_str)
})?;

View File

@ -20,9 +20,7 @@ impl DirList<'_> {
// Assume a maximum size for the database. This prevents bincode from
// throwing strange errors when it encounters invalid data.
const MAX_SIZE: u64 = 32 << 20; // 32 MiB
let deserializer = &mut bincode::options()
.with_fixint_encoding()
.with_limit(MAX_SIZE);
let deserializer = &mut bincode::options().with_fixint_encoding().with_limit(MAX_SIZE);
// Split bytes into sections.
let version_size = deserializer.serialized_size(&Self::VERSION).unwrap() as _;
@ -36,11 +34,9 @@ impl DirList<'_> {
let version = deserializer.deserialize(bytes_version)?;
match version {
Self::VERSION => Ok(deserializer.deserialize(bytes_dirs)?),
version => bail!(
"unsupported version (got {}, supports {})",
version,
Self::VERSION,
),
version => {
bail!("unsupported version (got {}, supports {})", version, Self::VERSION,)
}
}
})()
.context("could not deserialize database")
@ -159,11 +155,7 @@ mod tests {
#[test]
fn zero_copy() {
let dirs = DirList(vec![Dir {
path: "/".into(),
rank: 0.0,
last_accessed: 0,
}]);
let dirs = DirList(vec![Dir { path: "/".into(), rank: 0.0, last_accessed: 0 }]);
let bytes = dirs.to_bytes().unwrap();
let dirs = DirList::from_bytes(&bytes).unwrap();

View File

@ -1,26 +1,24 @@
mod dir;
mod query;
mod stream;
pub use dir::{Dir, DirList, Epoch, Rank};
pub use query::Matcher;
pub use stream::Stream;
use anyhow::{Context, Result};
use ordered_float::OrderedFloat;
use tempfile::{NamedTempFile, PersistError};
use std::borrow::Cow;
use std::cmp::Reverse;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
pub struct Database<'a> {
pub dirs: DirList<'a>,
pub struct Database<'file> {
pub dirs: DirList<'file>,
pub modified: bool,
data_dir: &'a Path,
data_dir: &'file PathBuf,
}
impl<'a> Database<'a> {
impl<'file> Database<'file> {
pub fn save(&mut self) -> Result<()> {
if !self.modified {
return Ok(());
@ -28,10 +26,7 @@ impl<'a> Database<'a> {
let buffer = self.dirs.to_bytes()?;
let mut file = NamedTempFile::new_in(&self.data_dir).with_context(|| {
format!(
"could not create temporary database in: {}",
self.data_dir.display()
)
format!("could not create temporary database in: {}", self.data_dir.display())
})?;
// Preallocate enough space on the file, preventing copying later on.
@ -39,10 +34,7 @@ impl<'a> Database<'a> {
// ignore it and proceed.
let _ = file.as_file().set_len(buffer.len() as _);
file.write_all(&buffer).with_context(|| {
format!(
"could not write to temporary database: {}",
file.path().display()
)
format!("could not write to temporary database: {}", file.path().display())
})?;
let path = db_path(&self.data_dir);
@ -58,11 +50,9 @@ impl<'a> Database<'a> {
let path = path.as_ref();
match self.dirs.iter_mut().find(|dir| dir.path == path) {
None => self.dirs.push(Dir {
path: Cow::Owned(path.into()),
last_accessed: now,
rank: 1.0,
}),
None => {
self.dirs.push(Dir { path: Cow::Owned(path.into()), last_accessed: now, rank: 1.0 })
}
Some(dir) => {
dir.last_accessed = now;
dir.rank += 1.0;
@ -72,12 +62,9 @@ impl<'a> Database<'a> {
self.modified = true;
}
pub fn iter<'i>(&'i mut self, m: &'i Matcher, now: Epoch) -> impl Iterator<Item = &'i Dir> {
self.dirs
.sort_unstable_by_key(|dir| Reverse(OrderedFloat(dir.score(now))));
self.dirs
.iter()
.filter(move |dir| m.matches(dir.path.as_ref()))
// Streaming iterator for directories.
pub fn stream(&mut self, now: Epoch) -> Stream<'_, 'file> {
Stream::new(self, now)
}
/// Removes the directory with `path` from the store.
@ -159,16 +146,13 @@ fn persist<P: AsRef<Path>>(file: NamedTempFile, path: P) -> Result<(), PersistEr
}
pub struct DatabaseFile {
data_dir: PathBuf,
buffer: Vec<u8>,
data_dir: PathBuf,
}
impl DatabaseFile {
pub fn new<P: Into<PathBuf>>(data_dir: P) -> DatabaseFile {
DatabaseFile {
data_dir: data_dir.into(),
buffer: Vec::new(),
}
pub fn new<P: Into<PathBuf>>(data_dir: P) -> Self {
DatabaseFile { buffer: Vec::new(), data_dir: data_dir.into() }
}
pub fn open(&mut self) -> Result<Database> {
@ -182,27 +166,16 @@ impl DatabaseFile {
let dirs = DirList::from_bytes(&self.buffer).with_context(|| {
format!("could not deserialize database: {}", path.display())
})?;
Ok(Database {
dirs,
modified: false,
data_dir: &self.data_dir,
})
Ok(Database { dirs, modified: false, data_dir: &self.data_dir })
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
// Create data directory, but don't create any file yet.
// The file will be created later by [`Database::save`]
// if any data is modified.
fs::create_dir_all(&self.data_dir).with_context(|| {
format!(
"unable to create data directory: {}",
self.data_dir.display()
)
format!("unable to create data directory: {}", self.data_dir.display())
})?;
Ok(Database {
dirs: DirList::new(),
modified: false,
data_dir: &self.data_dir,
})
Ok(Database { dirs: DirList::new(), modified: false, data_dir: &self.data_dir })
}
Err(e) => {
Err(e).with_context(|| format!("could not read from database: {}", path.display()))
@ -222,11 +195,7 @@ mod tests {
#[test]
fn add() {
let path = if cfg!(windows) {
r"C:\foo\bar"
} else {
"/foo/bar"
};
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
let now = 946684800;
let data_dir = tempfile::tempdir().unwrap();
@ -249,11 +218,7 @@ mod tests {
#[test]
fn remove() {
let path = if cfg!(windows) {
r"C:\foo\bar"
} else {
"/foo/bar"
};
let path = if cfg!(windows) { r"C:\foo\bar" } else { "/foo/bar" };
let now = 946684800;
let data_dir = tempfile::tempdir().unwrap();

View File

@ -1,111 +0,0 @@
use crate::util;
use std::fs;
use std::path;
#[derive(Debug, Default)]
pub struct Matcher {
keywords: Vec<String>,
check_exists: bool,
resolve_symlinks: bool,
}
impl Matcher {
pub fn new() -> Matcher {
Matcher::default()
}
pub fn with_exists(mut self, resolve_symlinks: bool) -> Matcher {
self.check_exists = true;
self.resolve_symlinks = resolve_symlinks;
self
}
pub fn with_keywords<S: AsRef<str>>(mut self, keywords: &[S]) -> Matcher {
self.keywords = keywords.iter().map(util::to_lowercase).collect();
self
}
pub fn matches<S: AsRef<str>>(&self, path: S) -> bool {
self.matches_keywords(&path) && self.matches_exists(path)
}
fn matches_exists<S: AsRef<str>>(&self, path: S) -> bool {
if !self.check_exists {
return true;
}
let resolver = if self.resolve_symlinks {
fs::symlink_metadata
} else {
fs::metadata
};
resolver(path.as_ref())
.map(|m| m.is_dir())
.unwrap_or_default()
}
fn matches_keywords<S: AsRef<str>>(&self, path: S) -> bool {
let (keywords_last, keywords) = match self.keywords.split_last() {
Some(split) => split,
None => return true,
};
let path = util::to_lowercase(path);
let mut path = path.as_str();
match path.rfind(keywords_last) {
Some(idx) => {
if path[idx + keywords_last.len()..].contains(path::is_separator) {
return false;
}
path = &path[..idx];
}
None => return false,
}
for keyword in keywords.iter().rev() {
match path.rfind(keyword) {
Some(idx) => path = &path[..idx],
None => return false,
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::Matcher;
#[test]
fn query() {
const CASES: &[(&[&str], &str, bool)] = &[
// Case normalization
(&["fOo", "bAr"], "/foo/bar", true),
// Last component
(&["ba"], "/foo/bar", true),
(&["fo"], "/foo/bar", false),
// Slash as suffix
(&["foo/"], "/foo", false),
(&["foo/"], "/foo/bar", true),
(&["foo/"], "/foo/bar/baz", false),
(&["foo", "/"], "/foo", false),
(&["foo", "/"], "/foo/bar", true),
(&["foo", "/"], "/foo/bar/baz", true),
// Split components
(&["/", "fo", "/", "ar"], "/foo/bar", true),
(&["oo/ba"], "/foo/bar", true),
// Overlap
(&["foo", "o", "bar"], "/foo/bar", false),
(&["/foo/", "/bar"], "/foo/bar", false),
(&["/foo/", "/bar"], "/foo/baz/bar", true),
];
for &(keywords, path, is_match) in CASES {
let matcher = Matcher::new().with_keywords(keywords);
assert_eq!(is_match, matcher.matches(path))
}
}
}

161
src/db/stream.rs Normal file
View File

@ -0,0 +1,161 @@
use super::{Database, Dir, Epoch};
use crate::util;
use ordered_float::OrderedFloat;
use std::fs;
use std::iter::Rev;
use std::ops::Range;
use std::path;
pub struct Stream<'db, 'file> {
db: &'db mut Database<'file>,
idxs: Rev<Range<usize>>,
keywords: Vec<String>,
check_exists: bool,
expire_below: Epoch,
resolve_symlinks: bool,
exclude_path: Option<String>,
}
impl<'db, 'file> Stream<'db, 'file> {
pub fn new(db: &'db mut Database<'file>, now: Epoch) -> Self {
// Iterate in descending order of score.
db.dirs.sort_unstable_by_key(|dir| OrderedFloat(dir.score(now)));
let idxs = (0..db.dirs.len()).rev();
// If a directory is deleted and hasn't been used for 90 days, delete
// it from the database.
let expire_below = now.saturating_sub(90 * 24 * 60 * 60);
Stream {
db,
idxs,
keywords: Vec::new(),
check_exists: false,
expire_below,
resolve_symlinks: false,
exclude_path: None,
}
}
pub fn with_exclude<S: Into<String>>(mut self, path: S) -> Self {
self.exclude_path = Some(path.into());
self
}
pub fn with_exists(mut self, resolve_symlinks: bool) -> Self {
self.check_exists = true;
self.resolve_symlinks = resolve_symlinks;
self
}
pub fn with_keywords<S: AsRef<str>>(mut self, keywords: &[S]) -> Self {
self.keywords = keywords.iter().map(util::to_lowercase).collect();
self
}
pub fn next(&mut self) -> Option<&Dir<'file>> {
while let Some(idx) = self.idxs.next() {
let dir = &self.db.dirs[idx];
if !self.matches_keywords(&dir.path) {
continue;
}
if !self.matches_exists(&dir.path) {
if dir.last_accessed < self.expire_below {
self.db.dirs.swap_remove(idx);
self.db.modified = true;
}
continue;
}
if Some(dir.path.as_ref()) == self.exclude_path.as_deref() {
continue;
}
let dir = &self.db.dirs[idx];
return Some(dir);
}
None
}
fn matches_exists<S: AsRef<str>>(&self, path: S) -> bool {
if !self.check_exists {
return true;
}
let resolver = if self.resolve_symlinks { fs::symlink_metadata } else { fs::metadata };
resolver(path.as_ref()).map(|m| m.is_dir()).unwrap_or_default()
}
fn matches_keywords<S: AsRef<str>>(&self, path: S) -> bool {
let (keywords_last, keywords) = match self.keywords.split_last() {
Some(split) => split,
None => return true,
};
let path = util::to_lowercase(path);
let mut path = path.as_str();
match path.rfind(keywords_last) {
Some(idx) => {
if path[idx + keywords_last.len()..].contains(path::is_separator) {
return false;
}
path = &path[..idx];
}
None => return false,
}
for keyword in keywords.iter().rev() {
match path.rfind(keyword) {
Some(idx) => path = &path[..idx],
None => return false,
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::Database;
use rstest::rstest;
use std::path::PathBuf;
#[rstest]
// Case normalization
#[case(&["fOo", "bAr"], "/foo/bar", true)]
// Last component
#[case(&["ba"], "/foo/bar", true)]
#[case(&["fo"], "/foo/bar", false)]
// Slash as suffix
#[case(&["foo/"], "/foo", false)]
#[case(&["foo/"], "/foo/bar", true)]
#[case(&["foo/"], "/foo/bar/baz", false)]
#[case(&["foo", "/"], "/foo", false)]
#[case(&["foo", "/"], "/foo/bar", true)]
#[case(&["foo", "/"], "/foo/bar/baz", true)]
// Split components
#[case(&["/", "fo", "/", "ar"], "/foo/bar", true)]
#[case(&["oo/ba"], "/foo/bar", true)]
// Overlap
#[case(&["foo", "o", "bar"], "/foo/bar", false)]
#[case(&["/foo/", "/bar"], "/foo/bar", false)]
#[case(&["/foo/", "/bar"], "/foo/baz/bar", true)]
fn query(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) {
let mut db =
Database { dirs: Vec::new().into(), modified: false, data_dir: &PathBuf::new() };
let stream = db.stream(0).with_keywords(keywords);
assert_eq!(is_match, stream.matches_keywords(path));
}
}

View File

@ -16,10 +16,7 @@ impl Fzf {
if multiple {
command.arg("-m");
}
command
.arg("-n2..")
.stdin(Stdio::piped())
.stdout(Stdio::piped());
command.arg("-n2..").stdin(Stdio::piped()).stdout(Stdio::piped());
if let Some(fzf_opts) = config::zo_fzf_opts() {
command.env("FZF_DEFAULT_OPTS", fzf_opts);
}
@ -41,10 +38,7 @@ impl Fzf {
}
pub fn wait_select(self) -> Result<String> {
let output = self
.child
.wait_with_output()
.context("wait failed on fzf")?;
let output = self.child.wait_with_output().context("wait failed on fzf")?;
match output.status.code() {
// normal exit

View File

@ -39,329 +39,369 @@ mod tests {
use askama::Template;
use assert_cmd::Command;
use once_cell::sync::OnceCell;
use seq_macro::seq;
use rstest::rstest;
macro_rules! with_opts_size {
($macro:ident) => {
$macro!(24);
};
#[rstest]
fn bash_bash(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Bash(&opts).render().unwrap();
Command::new("bash")
.args(&["--noprofile", "--norc", "-c", &source])
.assert()
.success()
.stdout("")
.stderr("");
}
fn opts() -> &'static [Opts<'static>] {
static OPTS: OnceCell<Vec<Opts>> = OnceCell::new();
const BOOLS: &[bool] = &[false, true];
const HOOKS: &[InitHook] = &[InitHook::None, InitHook::Prompt, InitHook::Pwd];
const CMDS: &[Option<&str>] = &[None, Some("z")];
#[rstest]
fn bash_shellcheck(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Bash(&opts).render().unwrap();
OPTS.get_or_init(|| {
let mut opts = Vec::new();
for &echo in BOOLS {
for &resolve_symlinks in BOOLS {
for &hook in HOOKS {
for &cmd in CMDS {
opts.push(Opts {
cmd,
hook,
echo,
resolve_symlinks,
});
}
}
}
}
// Verify that the value hardcoded into `with_opts_size` is correct.
macro_rules! id {
($x:literal) => {
$x
};
}
assert_eq!(opts.len(), with_opts_size!(id));
opts
})
Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "bash", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}
macro_rules! make_tests {
($N:literal) => {
seq!(i in 0..$N {
#[test]
fn bash_bash_#i() {
let opts = dbg!(&opts()[i]);
let source = Bash(opts).render().unwrap();
Command::new("bash")
.args(&["--noprofile", "--norc", "-c", &source])
.assert()
.success()
.stdout("")
.stderr("");
}
#[rstest]
fn bash_shfmt(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Bash(&opts).render().unwrap();
source.push('\n');
#[test]
fn bash_shellcheck_#i() {
let opts = dbg!(&opts()[i]);
let source = Bash(opts).render().unwrap();
Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "bash", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}
Command::new("shfmt")
.args(&["-d", "-s", "-ln", "bash", "-i", "4", "-ci", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}
#[test]
fn bash_shfmt_#i() {
let opts = dbg!(&opts()[i]);
let mut source = Bash(opts).render().unwrap();
source.push('\n');
#[rstest]
fn elvish_elvish(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = String::new();
Command::new("shfmt")
.args(&["-d", "-s", "-ln", "bash", "-i", "4", "-ci", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}
// Filter out lines using edit:*, since those functions
// are only available in the interactive editor.
for line in
Elvish(&opts).render().unwrap().split('\n').filter(|line| !line.contains("edit:"))
{
source.push_str(line);
source.push('\n');
}
#[test]
fn elvish_elvish_#i() {
let opts = dbg!(&opts()[i]);
let mut source = String::new();
Command::new("elvish")
.args(&["-c", &source, "-norc"])
.assert()
.success()
.stdout("")
.stderr("");
}
// Filter out lines using edit:*, since those functions
// are only available in the interactive editor.
for line in Elvish(opts)
.render()
.unwrap()
.split('\n')
.filter(|line| !line.contains("edit:"))
{
source.push_str(line);
source.push('\n');
}
#[rstest]
fn fish_fish(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Fish(&opts).render().unwrap();
Command::new("elvish")
.args(&["-c", &source, "-norc"])
.assert()
.success()
.stdout("")
.stderr("");
}
let tempdir = tempfile::tempdir().unwrap();
let tempdir = tempdir.path().to_str().unwrap();
#[test]
fn fish_fish_#i() {
let opts = dbg!(&opts()[i]);
let source = Fish(opts).render().unwrap();
Command::new("fish")
.env("HOME", tempdir)
.args(&["--command", &source, "--private"])
.assert()
.success()
.stdout("")
.stderr("");
}
let tempdir = tempfile::tempdir().unwrap();
let tempdir = tempdir.path().to_str().unwrap();
#[rstest]
fn fish_fishindent(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Fish(&opts).render().unwrap();
source.push('\n');
Command::new("fish")
.env("HOME", tempdir)
.args(&["--command", &source, "--private"])
.assert()
.success()
.stdout("")
.stderr("");
}
let tempdir = tempfile::tempdir().unwrap();
let tempdir = tempdir.path().to_str().unwrap();
#[test]
fn fish_fishindent_#i() {
let opts = dbg!(&opts()[i]);
let mut source = Fish(opts).render().unwrap();
source.push('\n');
Command::new("fish")
.env("HOME", tempdir)
.args(&["--command", "fish_indent", "--private"])
.write_stdin(source.to_string())
.assert()
.success()
.stdout(source)
.stderr("");
}
let tempdir = tempfile::tempdir().unwrap();
let tempdir = tempdir.path().to_str().unwrap();
#[rstest]
fn nushell_nushell(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Nushell(&opts).render().unwrap();
Command::new("fish")
.env("HOME", tempdir)
.args(&["--command", "fish_indent", "--private"])
.write_stdin(source.to_string())
.assert()
.success()
.stdout(source)
.stderr("");
}
let tempdir = tempfile::tempdir().unwrap();
let tempdir = tempdir.path().to_str().unwrap();
#[test]
fn nushell_nushell_#i() {
let opts = dbg!(&opts()[i]);
let source = Nushell(opts).render().unwrap();
let assert = Command::new("nu")
.env("HOME", tempdir)
.args(&["--commands", &source])
.assert()
.success()
.stderr("");
let tempdir = tempfile::tempdir().unwrap();
let tempdir = tempdir.path().to_str().unwrap();
let assert = Command::new("nu")
.env("HOME", tempdir)
.args(&["--commands", &source])
.assert()
.success()
.stderr("");
if opts.hook != InitHook::Pwd {
assert.stdout("");
}
}
#[test]
fn posix_bashposix_#i() {
let opts = dbg!(&opts()[i]);
let source = Posix(opts).render().unwrap();
let assert = Command::new("bash")
.args(&["--posix", "--noprofile", "--norc", "-c", &source])
.assert()
.success()
.stderr("");
if opts.hook != InitHook::Pwd {
assert.stdout("");
}
}
#[test]
fn posix_dash_#i() {
let opts = dbg!(&opts()[i]);
let source = Posix(opts).render().unwrap();
let assert = Command::new("dash")
.args(&["-c", &source])
.assert()
.success()
.stderr("");
if opts.hook != InitHook::Pwd {
assert.stdout("");
}
}
#[test]
fn posix_shellcheck_#i() {
let opts = dbg!(&opts()[i]);
let source = Posix(opts).render().unwrap();
Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "sh", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}
#[test]
fn posix_shfmt_#i() {
let opts = dbg!(&opts()[i]);
let mut source = Posix(opts).render().unwrap();
source.push('\n');
Command::new("shfmt")
.args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}
#[test]
fn powershell_pwsh_#i() {
let opts = dbg!(&opts()[i]);
let source = Powershell(opts).render().unwrap();
Command::new("pwsh")
.args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source])
.assert()
.success()
.stdout("")
.stderr("");
}
#[test]
fn xonsh_black_#i() {
let opts = dbg!(&opts()[i]);
let mut source = Xonsh(opts).render().unwrap();
source.push('\n');
Command::new("black")
.args(&["--check", "--diff", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("");
}
#[test]
fn xonsh_mypy_#i() {
let opts = dbg!(&opts()[i]);
let source = Xonsh(opts).render().unwrap();
Command::new("mypy")
.args(&["--command", &source])
.assert()
.success()
.stderr("");
}
#[test]
fn xonsh_pylint_#i() {
let opts = dbg!(&opts()[i]);
let mut source = Xonsh(opts).render().unwrap();
source.push('\n');
Command::new("pylint")
.args(&["--from-stdin", "zoxide"])
.write_stdin(source)
.assert()
.success()
.stderr("");
}
#[test]
fn xonsh_xonsh_#i() {
let opts = dbg!(&opts()[i]);
let source = Xonsh(opts).render().unwrap();
// We can't pass the source directly to `xonsh -c` due to
// a bug: <https://github.com/xonsh/xonsh/issues/3959>
Command::new("xonsh")
.args(&[
"-c",
"import sys; execx(sys.stdin.read(), 'exec', __xonsh__.ctx, filename='zoxide')",
"--no-rc"
])
.write_stdin(source.as_bytes())
.assert()
.success()
.stdout("")
.stderr("");
}
#[test]
fn zsh_shellcheck_#i() {
let opts = dbg!(&opts()[i]);
let source = Zsh(opts).render().unwrap();
// ShellCheck doesn't support zsh yet.
// https://github.com/koalaman/shellcheck/issues/809
Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "bash", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}
#[test]
fn zsh_zsh_#i() {
let opts = dbg!(&opts()[i]);
let source = Zsh(opts).render().unwrap();
Command::new("zsh")
.args(&["-c", &source, "--no-rcs"])
.assert()
.success()
.stdout("")
.stderr("");
}
});
if opts.hook != InitHook::Pwd {
assert.stdout("");
}
}
with_opts_size!(make_tests);
#[rstest]
fn posix_bashposix(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Posix(&opts).render().unwrap();
let assert = Command::new("bash")
.args(&["--posix", "--noprofile", "--norc", "-c", &source])
.assert()
.success()
.stderr("");
if opts.hook != InitHook::Pwd {
assert.stdout("");
}
}
#[rstest]
fn posix_dash(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Posix(&opts).render().unwrap();
let assert = Command::new("dash").args(&["-c", &source]).assert().success().stderr("");
if opts.hook != InitHook::Pwd {
assert.stdout("");
}
}
#[rstest]
fn posix_shellcheck_(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Posix(&opts).render().unwrap();
Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "sh", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}
#[rstest]
fn posix_shfmt(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Posix(&opts).render().unwrap();
source.push('\n');
Command::new("shfmt")
.args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}
#[rstest]
fn powershell_pwsh(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Powershell(&opts).render().unwrap();
Command::new("pwsh")
.args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source])
.assert()
.success()
.stdout("")
.stderr("");
}
#[rstest]
fn xonsh_black(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Xonsh(&opts).render().unwrap();
source.push('\n');
Command::new("black")
.args(&["--check", "--diff", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("");
}
#[rstest]
fn xonsh_mypy(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Xonsh(&opts).render().unwrap();
Command::new("mypy").args(&["--command", &source]).assert().success().stderr("");
}
#[rstest]
fn xonsh_pylint(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let mut source = Xonsh(&opts).render().unwrap();
source.push('\n');
Command::new("pylint")
.args(&["--from-stdin", "zoxide"])
.write_stdin(source)
.assert()
.success()
.stderr("");
}
#[rstest]
fn xonsh_xonsh(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Xonsh(&opts).render().unwrap();
// We can't pass the source directly to `xonsh -c` due to
// a bug: <https://github.com/xonsh/xonsh/issues/3959>
Command::new("xonsh")
.args(&[
"-c",
"import sys; execx(sys.stdin.read(), 'exec', __xonsh__.ctx, filename='zoxide')",
"--no-rc",
])
.write_stdin(source.as_bytes())
.assert()
.success()
.stdout("")
.stderr("");
}
#[rstest]
fn zsh_shellcheck(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Zsh(&opts).render().unwrap();
// ShellCheck doesn't support zsh yet.
// https://github.com/koalaman/shellcheck/issues/809
Command::new("shellcheck")
.args(&["--enable", "all", "--shell", "bash", "-"])
.write_stdin(source)
.assert()
.success()
.stdout("")
.stderr("");
}
#[rstest]
fn zsh_zsh(
#[values(None, Some("z"))] cmd: Option<&str>,
#[values(InitHook::None, InitHook::Prompt, InitHook::Pwd)] hook: InitHook,
#[values(false, true)] echo: bool,
#[values(false, true)] resolve_symlinks: bool,
) {
let opts = Opts { cmd, hook, echo, resolve_symlinks };
let source = Zsh(&opts).render().unwrap();
Command::new("zsh")
.args(&["-c", &source, "--no-rcs"])
.assert()
.success()
.stdout("")
.stderr("");
}
}

View File

@ -26,8 +26,7 @@ pub fn current_time() -> Result<Epoch> {
pub fn path_to_str<P: AsRef<Path>>(path: &P) -> Result<&str> {
let path = path.as_ref();
path.to_str()
.with_context(|| format!("invalid unicode in path: {}", path.display()))
path.to_str().with_context(|| format!("invalid unicode in path: {}", path.display()))
}
/// Resolves the absolute version of a path.

View File

@ -38,13 +38,7 @@ fn completions_fish() {
fn completions_powershell() {
let source = include_str!("../contrib/completions/_zoxide.ps1");
Command::new("pwsh")
.args(&[
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-Command",
source,
])
.args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", source])
.assert()
.success()
.stdout("")
@ -62,10 +56,5 @@ fn completions_zsh() {
compinit -u
"#;
Command::new("zsh")
.args(&["-c", source, "--no-rcs"])
.assert()
.success()
.stdout("")
.stderr("");
Command::new("zsh").args(&["-c", source, "--no-rcs"]).assert().success().stdout("").stderr("");
}