zoxide-{add,remove} should accept multiple arguments (#243)

This commit is contained in:
Ajeet D'Souza 2021-07-29 09:20:40 +05:30
parent 712d3403ef
commit 3bb21ede97
16 changed files with 134 additions and 107 deletions

View File

@ -1,3 +1,5 @@
<!-- markdownlint-disable MD024 -->
# 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.

70
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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].
<details>
@ -131,12 +131,12 @@ To install `zoxide`, use a package manager:
</details>
### 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
</details>
Now, initialize `zoxide` on your shell:
### *Step 4: Add `zoxide` to your shell*
To start using `zoxide`, add it to your shell.
<details>
<summary><code>bash</code></summary>
@ -195,7 +197,7 @@ zoxide init fish | source
</details>
<details>
<summary><code>nushell 0.32+</code></summary>
<summary><code>nushell v0.32+</code></summary>
Initialize the `zoxide` script:
@ -217,7 +219,7 @@ You can replace `__zoxide_prompt` with a custom prompt.
<details>
<summary><code>powershell</code></summary>
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)"
</details>
<details>
<summary>Any POSIX shell</summary>
<summary>any POSIX shell</summary>
Add this to your configuration:

View File

@ -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

View File

@ -51,7 +51,7 @@ _zoxide() {
;;
zoxide__add)
opts=" -h --help <path> "
opts=" -h --help <paths>... "
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 <path> "
opts=" -i -h --interactive --help <paths>... "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0

View File

@ -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

View File

@ -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

View File

@ -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).

View File

@ -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<PathBuf>,
}
/// 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<Vec<String>>,
#[clap(
conflicts_with = "interactive",
required_unless_present = "interactive",
value_hint = ValueHint::DirPath
)]
pub path: Option<String>,
pub paths: Vec<String>,
}

View File

@ -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(())
}

View File

@ -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()
}
}

View File

@ -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);

View File

@ -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()
}
}

View File

@ -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<P: AsRef<Path>>(mut file: NamedTempFile, path: P) -> Result<(), PersistError> {
use rand::distributions::{Distribution, Uniform};
@ -168,7 +158,7 @@ fn persist<P: AsRef<Path>>(mut file: NamedTempFile, path: P) -> Result<(), Persi
#[cfg(unix)]
fn persist<P: AsRef<Path>>(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();
}
}
}

View File

@ -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" })