From d49a2c14959a5df5cad022466e9efef0a573efb2 Mon Sep 17 00:00:00 2001 From: Anthony Ruhier Date: Wed, 1 Jul 2020 14:21:20 +0200 Subject: [PATCH] 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. --- README.md | 1 + src/config.rs | 7 ++ src/subcommand/add.rs | 11 +- src/subcommand/import.rs | 11 +- src/subcommand/init/shell/bash.rs | 4 +- src/subcommand/init/shell/fish.rs | 4 +- src/subcommand/init/shell/posix.rs | 4 +- src/subcommand/init/shell/powershell.rs | 4 +- src/subcommand/init/shell/zsh.rs | 4 +- src/subcommand/remove.rs | 2 +- src/util.rs | 135 +++++++++++++++++++++++- 11 files changed, 166 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ebe6b04..6e3a334 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/config.rs b/src/config.rs index 1190517..b42b76f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -52,3 +52,10 @@ pub fn zo_maxage() -> Result { None => Ok(10000.0), } } + +pub fn zo_resolve_symlinks() -> bool { + match env::var_os("_ZO_RESOLVE_SYMLINKS") { + Some(var) => var == "1", + None => false, + } +} diff --git a/src/subcommand/add.rs b/src/subcommand/add.rs index 043af52..3c3010d 100644 --- a/src/subcommand/add.rs +++ b/src/subcommand/add.rs @@ -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()?; ¤t_dir } }; @@ -32,7 +31,11 @@ impl Add { fn add>(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(()); diff --git a/src/subcommand/import.rs b/src/subcommand/import.rs index 4bf782a..03d3433 100644 --- a/src/subcommand/import.rs +++ b/src/subcommand/import.rs @@ -1,3 +1,4 @@ +use crate::config; use crate::db::{Db, Dir}; use crate::util; @@ -39,7 +40,7 @@ fn import>(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>(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::() .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 diff --git a/src/subcommand/init/shell/bash.rs b/src/subcommand/init/shell/bash.rs index 69c551a..c142108 100644 --- a/src/subcommand/init/shell/bash.rs +++ b/src/subcommand/init/shell/bash.rs @@ -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 } diff --git a/src/subcommand/init/shell/fish.rs b/src/subcommand/init/shell/fish.rs index a1e36c7..2c56631 100644 --- a/src/subcommand/init/shell/fish.rs +++ b/src/subcommand/init/shell/fish.rs @@ -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> { const HOOK_PWD: &str = r#" function _zoxide_hook --on-variable PWD - zoxide add + zoxide add "$(pwd -L)" end "#; diff --git a/src/subcommand/init/shell/posix.rs b/src/subcommand/init/shell/posix.rs index fe31229..631be21 100644 --- a/src/subcommand/init/shell/posix.rs +++ b/src/subcommand/init/shell/posix.rs @@ -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 }} diff --git a/src/subcommand/init/shell/powershell.rs b/src/subcommand/init/shell/powershell.rs index fef6b20..4b741ed 100644 --- a/src/subcommand/init/shell/powershell.rs +++ b/src/subcommand/init/shell/powershell.rs @@ -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> { 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'" diff --git a/src/subcommand/init/shell/zsh.rs b/src/subcommand/init/shell/zsh.rs index 623d13b..bc171a5 100644 --- a/src/subcommand/init/shell/zsh.rs +++ b/src/subcommand/init/shell/zsh.rs @@ -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> { const HOOK_PWD: &str = r#" _zoxide_hook() { - zoxide add + zoxide add "$(pwd -L)" } chpwd_functions=(${chpwd_functions[@]} "_zoxide_hook") diff --git a/src/subcommand/remove.rs b/src/subcommand/remove.rs index bf99e2c..4fd26bc 100644 --- a/src/subcommand/remove.rs +++ b/src/subcommand/remove.rs @@ -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) { diff --git a/src/util.rs b/src/util.rs index 5c30e83..ec8e4dc 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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 { @@ -25,6 +25,135 @@ pub fn canonicalize>(path: &P) -> Result { 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>(path: P) -> Result { + 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>(path: P) -> Option { + 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 { + 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(¤t_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::(); + if !result.is_dir() { + bail!("could not resolve path: {}", result.display()); + } + Ok(result) +} + +pub fn get_current_dir() -> Result { + env::current_dir().context("could not get current path") +} + pub fn path_to_str>(path: &P) -> Result<&str> { let path = path.as_ref(); path.to_str()