From 3bb21ede976a2be51ec2d07d488e8c2d7433cfe6 Mon Sep 17 00:00:00 2001 From: Ajeet D'Souza <98ajeet@gmail.com> Date: Thu, 29 Jul 2021 09:20:40 +0530 Subject: [PATCH] zoxide-{add,remove} should accept multiple arguments (#243) --- CHANGELOG.md | 6 +++ Cargo.lock | 70 +++++++++++++++++++++------------ Cargo.toml | 9 +++-- README.md | 20 +++++----- contrib/completions/_zoxide | 4 +- contrib/completions/zoxide.bash | 4 +- man/zoxide-add.1 | 2 +- man/zoxide-init.1 | 4 +- man/zoxide-remove.1 | 2 +- src/app/_app.rs | 8 ++-- src/app/add.rs | 42 ++++++++++++-------- src/app/import.rs | 8 ++-- src/app/query.rs | 8 +++- src/app/remove.rs | 30 +++++--------- src/db/mod.rs | 20 ++++------ templates/powershell.txt | 4 +- 16 files changed, 134 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75629b7..2300c4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ + + # Changelog All notable changes to this project will be documented in this file. @@ -7,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- `zoxide add` and `zoxide remove` now accept multiple arguments. + ### Fixed - Nushell: errors on 0.33.0. diff --git a/Cargo.lock b/Cargo.lock index 9b2ee9f..cafafbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,10 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "anyhow" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" +checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" [[package]] name = "arrayvec" @@ -55,9 +57,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88b6bd5df287567ffdf4ddf4d33060048e1068308e5f62d81c6f9824a045a48" +checksum = "3d20831bd004dda4c7c372c19cdabff369f794a95e955b3f13fe460e3e1ae95f" dependencies = [ "bstr", "doc-comment", @@ -170,10 +172,10 @@ dependencies = [ ] [[package]] -name = "difference" -version = "2.0.0" +name = "difflib" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "dirs-next" @@ -208,6 +210,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "funty" version = "1.1.0" @@ -233,9 +241,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "heck" @@ -248,23 +256,32 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "indexmap" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", "hashbrown", ] +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -286,9 +303,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.97" +version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" [[package]] name = "memchr" @@ -320,9 +337,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "2.5.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f100fcfb41e5385e0991f74981732049f9b896821542a219420491046baafdc2" +checksum = "039f02eb0f69271f26abe3202189275d7aa2258b903cb0281b5de710a2570ff3" dependencies = [ "num-traits", ] @@ -350,11 +367,12 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "predicates" -version = "1.0.8" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df" +checksum = "bc3d91237f5de3bcd9d927e24d03b495adb6135097b001cea7403e2d573d00a9" dependencies = [ - "difference", + "difflib", + "itertools", "predicates-core", ] @@ -400,9 +418,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" dependencies = [ "unicode-xid", ] @@ -576,9 +594,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" dependencies = [ "proc-macro2", "quote", @@ -637,9 +655,9 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "unicode-segmentation" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-width" diff --git a/Cargo.toml b/Cargo.toml index 35c6f48..b30a3e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,18 +11,21 @@ categories = ["command-line-utilities", "filesystem"] [dependencies] anyhow = "1.0.32" -askama = { version="0.10.3", default-features=false } +askama = { version = "0.10.3", default-features = false } bincode = "1.3.1" clap = "3.0.0-beta.2" dirs-next = "2.0.0" dunce = "1.0.1" glob = "0.3.0" ordered-float = "2.0.0" -serde = { version="1.0.116", features=["derive"] } +serde = { version = "1.0.116", features = ["derive"] } tempfile = "3.1.0" [target.'cfg(windows)'.dependencies] -rand = { version="0.8.4", features=["getrandom", "small_rng"], default-features=false } +rand = { version = "0.8.4", features = [ + "getrandom", + "small_rng", +], default-features = false } [dev-dependencies] assert_cmd = "1.0.1" diff --git a/README.md b/README.md index 0732449..44857ac 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ Read more about the matching algorithm [here][algorithm-matching]. ## Getting started -### Step 1: Install `zoxide` +### *Step 1: Install `zoxide`* -`zoxide` supports most major platforms. If your platform isn't listed below, +`zoxide` runs on most major platforms. If your platform isn't listed below, please [open an issue][issues].
@@ -131,12 +131,12 @@ To install `zoxide`, use a package manager:
-### Step 2: Install `fzf` (optional) +### *Step 2: Install `fzf` (optional)* [`fzf`][fzf] is a command-line fuzzy finder, used by `zoxide` for interactive -selection ([installation instructions][fzf-installation]). +selection. It can be installed from [here][fzf-installation]. -### Step 3: Add `zoxide` to your shell +### *Step 3: Import your data (optional)* If you currently use any of the following utilities, you may want to import your data into `zoxide`: @@ -159,7 +159,9 @@ zoxide import --from z path/to/db -Now, initialize `zoxide` on your shell: +### *Step 4: Add `zoxide` to your shell* + +To start using `zoxide`, add it to your shell.
bash @@ -195,7 +197,7 @@ zoxide init fish | source
-nushell 0.32+ +nushell v0.32+ Initialize the `zoxide` script: @@ -217,7 +219,7 @@ You can replace `__zoxide_prompt` with a custom prompt.
powershell -Add this to your configuration (the location is stored in `$profile`): +Add this to your configuration (find it with `echo $profile`): ```powershell Invoke-Expression (& { @@ -251,7 +253,7 @@ eval "$(zoxide init zsh)"
-Any POSIX shell +any POSIX shell Add this to your configuration: diff --git a/contrib/completions/_zoxide b/contrib/completions/_zoxide index 0865087..1548e9b 100644 --- a/contrib/completions/_zoxide +++ b/contrib/completions/_zoxide @@ -32,7 +32,7 @@ _zoxide() { _arguments "${_arguments_options[@]}" \ '-h[Prints help information]' \ '--help[Prints help information]' \ -':path:_files -/' \ +'*::paths:_files -/' \ && ret=0 ;; (import) @@ -75,7 +75,7 @@ _arguments "${_arguments_options[@]}" \ '()*--interactive=[]' \ '-h[Prints help information]' \ '--help[Prints help information]' \ -'::path:_files -/' \ +'*::paths:_files -/' \ && ret=0 ;; esac diff --git a/contrib/completions/zoxide.bash b/contrib/completions/zoxide.bash index 2ab89f9..749b1ef 100644 --- a/contrib/completions/zoxide.bash +++ b/contrib/completions/zoxide.bash @@ -51,7 +51,7 @@ _zoxide() { ;; zoxide__add) - opts=" -h --help " + opts=" -h --help ... " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -127,7 +127,7 @@ _zoxide() { return 0 ;; zoxide__remove) - opts=" -i -h --interactive --help " + opts=" -i -h --interactive --help ... " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/man/zoxide-add.1 b/man/zoxide-add.1 index 707fca8..d82b12a 100644 --- a/man/zoxide-add.1 +++ b/man/zoxide-add.1 @@ -2,7 +2,7 @@ .SH NAME zoxide-add - add a new directory or increment its rank .SH SYNOPSIS -.B zoxide add \fIPATH\fR +.B zoxide add \fI[PATHS]\fR .SH DESCRIPTION If the directory is not already in the database, this command creates a new entry for it with a default score of \fI1\fR, otherwise, it increments the diff --git a/man/zoxide-init.1 b/man/zoxide-init.1 index 2286443..3efffbf 100644 --- a/man/zoxide-init.1 +++ b/man/zoxide-init.1 @@ -44,7 +44,7 @@ Add this to your configuration (usually \fI~/.config/nu/config.toml\fR): You can replace \fB__zoxide_prompt\fR with a custom prompt. .TP .B powershell -Add this to your configuration (the location is stored in \fI$profile\fR): +Add this to your configuration (find it with \fIecho $profile\fR): .sp .nf \fBInvoke-Expression (& { @@ -67,7 +67,7 @@ Add this to your configuration (usually \fI~/.zshrc\fR): \fBeval "$(zoxide init zsh)"\fR .fi .TP -.B Any POSIX shell +.B any POSIX shell .sp Add this to your configuration: .sp diff --git a/man/zoxide-remove.1 b/man/zoxide-remove.1 index f0f186e..ee71640 100644 --- a/man/zoxide-remove.1 +++ b/man/zoxide-remove.1 @@ -2,7 +2,7 @@ .SH NAME zoxide-remove - remove a directory from the database .SH SYNOPSIS -.B zoxide remove \fIPATH [OPTIONS]\fR +.B zoxide remove \fI[PATHS] [OPTIONS]\fR .SH DESCRIPTION If you'd like to permanently exclude a directory from the database, see the \fB_ZO_EXCLUDE_DIRS\fR environment variable in \fBzoxide\fR(1). diff --git a/src/app/_app.rs b/src/app/_app.rs index 7362a24..0ce7b2a 100644 --- a/src/app/_app.rs +++ b/src/app/_app.rs @@ -33,8 +33,8 @@ pub enum App { /// Add a new directory or increment its rank #[derive(Clap, Debug)] pub struct Add { - #[clap(value_hint = ValueHint::DirPath)] - pub path: PathBuf, + #[clap(min_values = 1, required = true, value_hint = ValueHint::DirPath)] + pub paths: Vec, } /// Import entries from another application @@ -126,12 +126,12 @@ pub struct Query { #[derive(Clap, Debug)] pub struct Remove { // Use interactive selection - #[clap(conflicts_with = "path", long, short, value_name = "keywords")] + #[clap(conflicts_with = "paths", long, short, value_name = "keywords")] pub interactive: Option>, #[clap( conflicts_with = "interactive", required_unless_present = "interactive", value_hint = ValueHint::DirPath )] - pub path: Option, + pub paths: Vec, } diff --git a/src/app/add.rs b/src/app/add.rs index 7dff860..7f73400 100644 --- a/src/app/add.rs +++ b/src/app/add.rs @@ -9,33 +9,41 @@ use std::path::Path; impl Run for Add { fn run(&self) -> Result<()> { - let path = if config::resolve_symlinks() { - util::canonicalize(&self.path) - } else { - util::resolve_path(&self.path) - }?; - let path = util::path_to_str(&path)?; - let now = util::current_time()?; - // These characters can't be printed cleanly to a single line, so they // can cause confusion when writing to fzf / stdout. const EXCLUDE_CHARS: &[char] = &['\n', '\r']; - let mut exclude_dirs = config::exclude_dirs()?.into_iter(); - if exclude_dirs.any(|pattern| pattern.matches(path)) || path.contains(EXCLUDE_CHARS) { - return Ok(()); - } - if !Path::new(path).is_dir() { - bail!("not a directory: {}", path); - } let data_dir = config::data_dir()?; + let exclude_dirs = config::exclude_dirs()?; let max_age = config::maxage()?; + let now = util::current_time()?; let mut db = DatabaseFile::new(data_dir); let mut db = db.open()?; - db.add(path, now); - db.age(max_age); + for path in self.paths.iter() { + let path = if config::resolve_symlinks() { + util::canonicalize(path) + } else { + util::resolve_path(path) + }?; + let path = util::path_to_str(&path)?; + + // Ignore path if it contains unsupported characters, or if it's in + // the exclude list. + if path.contains(EXCLUDE_CHARS) || exclude_dirs.iter().any(|glob| glob.matches(path)) { + continue; + } + if !Path::new(path).is_dir() { + bail!("not a directory: {}", path); + } + db.add(path, now); + } + + if db.modified { + db.age(max_age); + db.save()?; + } Ok(()) } diff --git a/src/app/import.rs b/src/app/import.rs index 128c3ec..2b23749 100644 --- a/src/app/import.rs +++ b/src/app/import.rs @@ -8,7 +8,7 @@ use std::fs; impl Run for Import { fn run(&self) -> Result<()> { - let buffer = &fs::read_to_string(&self.path).with_context(|| { + let buffer = fs::read_to_string(&self.path).with_context(|| { format!("could not open database for importing: {}", &self.path.display()) })?; @@ -20,12 +20,12 @@ impl Run for Import { } match self.from { - ImportFrom::Autojump => from_autojump(db, buffer), - ImportFrom::Z => from_z(db, buffer), + ImportFrom::Autojump => from_autojump(db, &buffer), + ImportFrom::Z => from_z(db, &buffer), } .context("import error")?; - Ok(()) + db.save() } } diff --git a/src/app/query.rs b/src/app/query.rs index 3dfee24..5abd28d 100644 --- a/src/app/query.rs +++ b/src/app/query.rs @@ -1,6 +1,6 @@ use crate::app::{Query, Run}; use crate::config; -use crate::db::DatabaseFile; +use crate::db::{Database, DatabaseFile}; use crate::error::BrokenPipeHandler; use crate::fzf::Fzf; use crate::util; @@ -14,6 +14,12 @@ impl Run for Query { let data_dir = config::data_dir()?; let mut db = DatabaseFile::new(data_dir); let mut db = db.open()?; + self.query(&mut db).and(db.save()) + } +} + +impl Query { + fn query(&self, db: &mut Database) -> Result<()> { let now = util::current_time()?; let mut stream = db.stream(now).with_keywords(&self.keywords); diff --git a/src/app/remove.rs b/src/app/remove.rs index 99eb48c..8d83264 100644 --- a/src/app/remove.rs +++ b/src/app/remove.rs @@ -28,35 +28,25 @@ impl Run for Remove { selection = fzf.wait_select()?; let paths = selection.lines().filter_map(|line| line.get(5..)); - let mut not_found = Vec::new(); for path in paths { - if !db.remove(&path) { - not_found.push(path); + if !db.remove(path) { + bail!("path not found in database: {}", path); } } - - if !not_found.is_empty() { - let mut err = "path not found in database:".to_string(); - for path in not_found { - err.push_str("\n "); - err.push_str(path.as_ref()); - } - bail!(err); - } } None => { - // unwrap is safe here because path is required_unless_present = "interactive" - let path = self.path.as_ref().unwrap(); - if !db.remove(path) { - let path_abs = util::resolve_path(&path)?; - let path_abs = util::path_to_str(&path_abs)?; - if path_abs != path && !db.remove(path) { - bail!("path not found in database:\n {}", &path) + for path in self.paths.iter() { + if !db.remove(path) { + let path_abs = util::resolve_path(path)?; + let path_abs = util::path_to_str(&path_abs)?; + if path_abs != path && !db.remove(path_abs) { + bail!("path not found in database: {} ({})", path, path_abs) + } } } } } - Ok(()) + db.save() } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 6808f06..3d198a3 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -15,7 +15,7 @@ use std::path::{Path, PathBuf}; pub struct Database<'file> { pub dirs: DirList<'file>, pub modified: bool, - pub data_dir: &'file PathBuf, + pub data_dir: &'file Path, } impl<'file> Database<'file> { @@ -25,7 +25,7 @@ impl<'file> Database<'file> { } let buffer = self.dirs.to_bytes()?; - let mut file = NamedTempFile::new_in(&self.data_dir).with_context(|| { + let mut file = NamedTempFile::new_in(self.data_dir).with_context(|| { format!("could not create temporary database in: {}", self.data_dir.display()) })?; @@ -125,16 +125,6 @@ impl<'file> Database<'file> { } } -impl Drop for Database<'_> { - fn drop(&mut self) { - // Since the error can't be properly handled here, - // pretty-print it instead. - if let Err(e) = self.save() { - let _ = writeln!(io::stderr(), "zoxide: {:?}", e); - } - } -} - #[cfg(windows)] fn persist>(mut file: NamedTempFile, path: P) -> Result<(), PersistError> { use rand::distributions::{Distribution, Uniform}; @@ -168,7 +158,7 @@ fn persist>(mut file: NamedTempFile, path: P) -> Result<(), Persi #[cfg(unix)] fn persist>(file: NamedTempFile, path: P) -> Result<(), PersistError> { - file.persist(&path)?; + file.persist(path)?; Ok(()) } @@ -231,6 +221,7 @@ mod tests { let mut db = db.open().unwrap(); db.add(path, now); db.add(path, now); + db.save().unwrap(); } { let mut db = DatabaseFile::new(data_dir.path()); @@ -253,17 +244,20 @@ mod tests { let mut db = DatabaseFile::new(data_dir.path()); let mut db = db.open().unwrap(); db.add(path, now); + db.save().unwrap(); } { let mut db = DatabaseFile::new(data_dir.path()); let mut db = db.open().unwrap(); assert!(db.remove(path)); + db.save().unwrap(); } { let mut db = DatabaseFile::new(data_dir.path()); let mut db = db.open().unwrap(); assert!(db.dirs.is_empty()); assert!(!db.remove(path)); + db.save().unwrap(); } } } diff --git a/templates/powershell.txt b/templates/powershell.txt index ede8619..2d30a40 100644 --- a/templates/powershell.txt +++ b/templates/powershell.txt @@ -115,7 +115,7 @@ Set-Alias {{cmd}}i __zoxide_zi {%- endmatch %} {{ section }} -# To initialize zoxide, add this to your configuration (the location is stored -# in $profile): +# To initialize zoxide, add this to your configuration (find it with +# `echo $profile`): # # Invoke-Expression (& { $hook = if ($PSVersionTable.PSVersion.Major -ge 6) { 'pwd' } else { 'prompt' } (zoxide init powershell --hook $hook) -join "`n" })