Add _ZO_RESOLVE_SYMLINKS to resolve or not symlinks (#85)

Fixes #80.

Disable by default the symlinks resolution, making a symlink and its
target 2 different entries in the database. Adds the
_ZO_RESOLVE_SYMLINKS env variable to re-enable it.

Example:
  /tmp/foo-target is a directory
  /tmp/foo symlinks to /tmp/foo-target

With _ZO_RESOLVE_SYMLINKS=1, `z add /tmp/foo` adds `/tmp/foo-target` in the database.

With _ZO_RESOLVE_SYMLINKS=0 or unset, `z add /tmp/foo` adds `/tmp/foo` in the database.
This commit is contained in:
Anthony Ruhier 2020-07-01 14:21:20 +02:00 committed by Ajeet D'Souza
parent c5bc49884d
commit d49a2c1495
11 changed files with 166 additions and 21 deletions

View File

@ -206,5 +206,6 @@ eval "$(zoxide init zsh)"
("`:`" on Linux/macOS, "`;`" on Windows) to be excluded from the database
- `$_ZO_FZF_OPTS`: custom flags to pass to `fzf`
- `$_ZO_MAXAGE`: sets the maximum total age after which entries start getting deleted
- `$_ZO_RESOLVE_SYMLINKS`: when set to `1`, `z add` will resolve symlinks.
[`dirs` documentation]: https://docs.rs/dirs/latest/dirs/fn.data_local_dir.html

View File

@ -52,3 +52,10 @@ pub fn zo_maxage() -> Result<Rank> {
None => Ok(10000.0),
}
}
pub fn zo_resolve_symlinks() -> bool {
match env::var_os("_ZO_RESOLVE_SYMLINKS") {
Some(var) => var == "1",
None => false,
}
}

View File

@ -2,10 +2,9 @@ use crate::config;
use crate::db::{Db, Dir, Rank};
use crate::util;
use anyhow::{Context, Result};
use anyhow::Result;
use structopt::StructOpt;
use std::env;
use std::path::{Path, PathBuf};
/// Add a new directory or increment its rank
@ -21,7 +20,7 @@ impl Add {
let path = match &self.path {
Some(path) => path,
None => {
current_dir = env::current_dir().context("unable to fetch current directory")?;
current_dir = util::get_current_dir()?;
&current_dir
}
};
@ -32,7 +31,11 @@ impl Add {
fn add<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
let path = util::canonicalize(&path)?;
let path = if config::zo_resolve_symlinks() {
util::canonicalize(&path)?
} else {
util::resolve_path(&path)?
};
if config::zo_exclude_dirs().contains(&path) {
return Ok(());

View File

@ -1,3 +1,4 @@
use crate::config;
use crate::db::{Db, Dir};
use crate::util;
@ -39,7 +40,7 @@ fn import<P: AsRef<Path>>(path: P, merge: bool) -> Result<()> {
.with_context(|| format!("could not read z database: {}", path.display()))?;
for (idx, line) in buffer.lines().enumerate() {
if let Err(e) = import_line(&mut db, line) {
if let Err(e) = import_line(&mut db, line, config::zo_resolve_symlinks()) {
let line_num = idx + 1;
eprintln!("Error on line {}: {}", line_num, e);
}
@ -51,7 +52,7 @@ fn import<P: AsRef<Path>>(path: P, merge: bool) -> Result<()> {
Ok(())
}
fn import_line(db: &mut Db, line: &str) -> Result<()> {
fn import_line(db: &mut Db, line: &str, resolve_symlinks: bool) -> Result<()> {
let mut split_line = line.rsplitn(3, '|');
let (path, epoch_str, rank_str) = (|| {
@ -70,7 +71,11 @@ fn import_line(db: &mut Db, line: &str) -> Result<()> {
.parse::<f64>()
.with_context(|| format!("invalid rank: {}", rank_str))?;
let path = util::canonicalize(&path)?;
let path = if resolve_symlinks {
util::canonicalize(&path)?
} else {
util::resolve_path(&path)?
};
let path = util::path_to_str(&path)?;
// If the path exists in the database, add the ranks and set the epoch to

View File

@ -15,7 +15,7 @@ pub const CONFIG: ShellConfig = ShellConfig {
const HOOK_PROMPT: &str = r#"
_zoxide_hook() {
zoxide add
zoxide add "$(pwd -L)"
}
case "$PROMPT_COMMAND" in
@ -31,7 +31,7 @@ _zoxide_hook() {
_ZO_PWD="${PWD}"
elif [ "${_ZO_PWD}" != "${PWD}" ]; then
_ZO_PWD="${PWD}"
zoxide add
zoxide add "$(pwd -L)"
fi
}

View File

@ -69,14 +69,14 @@ end
const HOOK_PROMPT: &str = r#"
function _zoxide_hook --on-event fish_prompt
zoxide add
zoxide add $(pwd -L)
end
"#;
const fn hook_pwd() -> Result<Cow<'static, str>> {
const HOOK_PWD: &str = r#"
function _zoxide_hook --on-variable PWD
zoxide add
zoxide add "$(pwd -L)"
end
"#;

View File

@ -68,7 +68,7 @@ alias {0}r='zoxide remove'
const HOOK_PROMPT: &str = r#"
_zoxide_hook() {
zoxide add
zoxide add "$(pwd -L)"
}
case "$PS1" in
@ -117,7 +117,7 @@ _zoxide_setpwd
_zoxide_hook() {{
_ZO_OLDPWD="$(cat "$_ZO_PWD_PATH")"
if [ -z "$_ZO_OLDPWD" ] || [ "$_ZO_OLDPWD" != "$PWD" ]; then
_zoxide_setpwd && zoxide add > /dev/null
_zoxide_setpwd && zoxide add "$(pwd -L)" > /dev/null
fi
}}

View File

@ -72,7 +72,7 @@ function {0}ri {{
const HOOK_PROMPT: &str = r#"
$PreZoxidePrompt = $function:prompt
function prompt {
$null = zoxide add
$null = zoxide add $(Get-Location)
& $PreZoxidePrompt
}
"#;
@ -81,7 +81,7 @@ const fn hook_pwd() -> Result<Cow<'static, str>> {
const HOOK_PWD: &str = r#"
if ($PSVersionTable.PSVersion.Major -ge 6) {
$ExecutionContext.InvokeCommand.LocationChangedAction = {
$null = zoxide add
$null = zoxide add $(Get-Location)
}
} else {
Write-Error "pwd hook requires pwsh - use 'zoxide init powershell --hook prompt'"

View File

@ -15,7 +15,7 @@ pub const CONFIG: ShellConfig = ShellConfig {
const HOOK_PROMPT: &str = r#"
_zoxide_hook() {
zoxide add
zoxide add "$(pwd -L)"
}
[[ -n "${precmd_functions[(r)_zoxide_hook]}" ]] || {
@ -26,7 +26,7 @@ _zoxide_hook() {
const fn hook_pwd() -> Result<Cow<'static, str>> {
const HOOK_PWD: &str = r#"
_zoxide_hook() {
zoxide add
zoxide add "$(pwd -L)"
}
chpwd_functions=(${chpwd_functions[@]} "_zoxide_hook")

View File

@ -25,7 +25,7 @@ fn remove(path: &str) -> Result<()> {
return Ok(());
}
let path = util::canonicalize(&path)?;
let path = util::resolve_path(&path)?;
let path = util::path_to_str(&path)?;
if let Some(idx) = db.dirs.iter().position(|dir| dir.path == path) {

View File

@ -1,9 +1,9 @@
use crate::config;
use crate::db::{Db, Epoch};
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use std::env;
use std::path::{Component, Path, PathBuf};
use std::time::SystemTime;
pub fn get_db() -> Result<Db> {
@ -25,6 +25,135 @@ pub fn canonicalize<P: AsRef<Path>>(path: &P) -> Result<PathBuf> {
dunce::canonicalize(path).with_context(|| format!("could not resolve path: {}", path.display()))
}
/// Resolves the absolute version of a path.
///
/// If path is already absolute, the path is still processed to be cleaned, as it can contained ".." or "." (or other)
/// character.
/// If path is relative, use the current directory to build the absolute path.
#[cfg(any(unix, windows))]
pub fn resolve_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = path.as_ref();
let base_path;
let mut components = path.components().peekable();
let mut stack = Vec::new();
// initialize root
if cfg!(unix) {
match components.peek() {
Some(Component::RootDir) => {
let root = components.next().unwrap();
stack.push(root);
}
_ => {
base_path = get_current_dir()?;
stack.extend(base_path.components());
}
}
} else if cfg!(windows) {
use std::path::Prefix;
fn get_drive_letter<P: AsRef<Path>>(path: P) -> Option<u8> {
let path = path.as_ref();
let mut components = path.components();
match components.next() {
Some(Component::Prefix(prefix)) => match prefix.kind() {
Prefix::Disk(drive_letter) | Prefix::VerbatimDisk(drive_letter) => {
Some(drive_letter)
}
_ => None,
},
_ => None,
}
}
fn get_drive_path(drive_letter: u8) -> PathBuf {
format!(r"{}:\", drive_letter as char).into()
}
fn get_drive_relative(drive_letter: u8) -> Result<PathBuf> {
let path = get_current_dir()?;
if Some(drive_letter) == get_drive_letter(&path) {
return Ok(path);
}
if let Some(path) = env::var_os(format!("={}:", drive_letter as char)) {
return Ok(path.into());
}
let path = get_drive_path(drive_letter);
Ok(path)
}
match components.peek() {
Some(Component::Prefix(prefix)) => match prefix.kind() {
Prefix::Disk(drive_letter) => {
let disk = components.next().unwrap();
match components.peek() {
Some(Component::RootDir) => {
let root = components.next().unwrap();
stack.push(disk);
stack.push(root);
}
_ => {
base_path = get_drive_relative(drive_letter)?;
stack.extend(base_path.components());
}
}
}
Prefix::VerbatimDisk(drive_letter) => {
components.next();
if components.peek() == Some(&Component::RootDir) {
components.next();
}
base_path = get_drive_path(drive_letter);
stack.extend(base_path.components());
}
_ => bail!("invalid path: {}", path.display()),
},
Some(Component::RootDir) => {
components.next();
let current_dir = env::current_dir()?;
let drive_letter = get_drive_letter(&current_dir).with_context(|| {
format!("could not get drive letter: {}", current_dir.display())
})?;
base_path = get_drive_path(drive_letter);
stack.extend(base_path.components());
}
_ => {
base_path = get_current_dir()?;
stack.extend(base_path.components());
}
}
}
for component in components {
match component {
Component::Normal(_) => stack.push(component),
Component::CurDir => (),
Component::ParentDir => {
if stack.last() != Some(&Component::RootDir) {
stack.pop();
}
}
Component::Prefix(_) | Component::RootDir => unreachable!(),
}
}
let result = stack.iter().collect::<PathBuf>();
if !result.is_dir() {
bail!("could not resolve path: {}", result.display());
}
Ok(result)
}
pub fn get_current_dir() -> Result<PathBuf> {
env::current_dir().context("could not get current path")
}
pub fn path_to_str<P: AsRef<Path>>(path: &P) -> Result<&str> {
let path = path.as_ref();
path.to_str()