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:
parent
c5bc49884d
commit
d49a2c1495
|
@ -206,5 +206,6 @@ eval "$(zoxide init zsh)"
|
||||||
("`:`" on Linux/macOS, "`;`" on Windows) to be excluded from the database
|
("`:`" on Linux/macOS, "`;`" on Windows) to be excluded from the database
|
||||||
- `$_ZO_FZF_OPTS`: custom flags to pass to `fzf`
|
- `$_ZO_FZF_OPTS`: custom flags to pass to `fzf`
|
||||||
- `$_ZO_MAXAGE`: sets the maximum total age after which entries start getting deleted
|
- `$_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
|
[`dirs` documentation]: https://docs.rs/dirs/latest/dirs/fn.data_local_dir.html
|
||||||
|
|
|
@ -52,3 +52,10 @@ pub fn zo_maxage() -> Result<Rank> {
|
||||||
None => Ok(10000.0),
|
None => Ok(10000.0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn zo_resolve_symlinks() -> bool {
|
||||||
|
match env::var_os("_ZO_RESOLVE_SYMLINKS") {
|
||||||
|
Some(var) => var == "1",
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,9 @@ use crate::config;
|
||||||
use crate::db::{Db, Dir, Rank};
|
use crate::db::{Db, Dir, Rank};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// Add a new directory or increment its rank
|
/// Add a new directory or increment its rank
|
||||||
|
@ -21,7 +20,7 @@ impl Add {
|
||||||
let path = match &self.path {
|
let path = match &self.path {
|
||||||
Some(path) => path,
|
Some(path) => path,
|
||||||
None => {
|
None => {
|
||||||
current_dir = env::current_dir().context("unable to fetch current directory")?;
|
current_dir = util::get_current_dir()?;
|
||||||
¤t_dir
|
¤t_dir
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -32,7 +31,11 @@ impl Add {
|
||||||
|
|
||||||
fn add<P: AsRef<Path>>(path: P) -> Result<()> {
|
fn add<P: AsRef<Path>>(path: P) -> Result<()> {
|
||||||
let path = path.as_ref();
|
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) {
|
if config::zo_exclude_dirs().contains(&path) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::config;
|
||||||
use crate::db::{Db, Dir};
|
use crate::db::{Db, Dir};
|
||||||
use crate::util;
|
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()))?;
|
.with_context(|| format!("could not read z database: {}", path.display()))?;
|
||||||
|
|
||||||
for (idx, line) in buffer.lines().enumerate() {
|
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;
|
let line_num = idx + 1;
|
||||||
eprintln!("Error on line {}: {}", line_num, e);
|
eprintln!("Error on line {}: {}", line_num, e);
|
||||||
}
|
}
|
||||||
|
@ -51,7 +52,7 @@ fn import<P: AsRef<Path>>(path: P, merge: bool) -> Result<()> {
|
||||||
Ok(())
|
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 mut split_line = line.rsplitn(3, '|');
|
||||||
|
|
||||||
let (path, epoch_str, rank_str) = (|| {
|
let (path, epoch_str, rank_str) = (|| {
|
||||||
|
@ -70,7 +71,11 @@ fn import_line(db: &mut Db, line: &str) -> Result<()> {
|
||||||
.parse::<f64>()
|
.parse::<f64>()
|
||||||
.with_context(|| format!("invalid rank: {}", rank_str))?;
|
.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)?;
|
let path = util::path_to_str(&path)?;
|
||||||
|
|
||||||
// If the path exists in the database, add the ranks and set the epoch to
|
// If the path exists in the database, add the ranks and set the epoch to
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub const CONFIG: ShellConfig = ShellConfig {
|
||||||
|
|
||||||
const HOOK_PROMPT: &str = r#"
|
const HOOK_PROMPT: &str = r#"
|
||||||
_zoxide_hook() {
|
_zoxide_hook() {
|
||||||
zoxide add
|
zoxide add "$(pwd -L)"
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$PROMPT_COMMAND" in
|
case "$PROMPT_COMMAND" in
|
||||||
|
@ -31,7 +31,7 @@ _zoxide_hook() {
|
||||||
_ZO_PWD="${PWD}"
|
_ZO_PWD="${PWD}"
|
||||||
elif [ "${_ZO_PWD}" != "${PWD}" ]; then
|
elif [ "${_ZO_PWD}" != "${PWD}" ]; then
|
||||||
_ZO_PWD="${PWD}"
|
_ZO_PWD="${PWD}"
|
||||||
zoxide add
|
zoxide add "$(pwd -L)"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,14 +69,14 @@ end
|
||||||
|
|
||||||
const HOOK_PROMPT: &str = r#"
|
const HOOK_PROMPT: &str = r#"
|
||||||
function _zoxide_hook --on-event fish_prompt
|
function _zoxide_hook --on-event fish_prompt
|
||||||
zoxide add
|
zoxide add $(pwd -L)
|
||||||
end
|
end
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const fn hook_pwd() -> Result<Cow<'static, str>> {
|
const fn hook_pwd() -> Result<Cow<'static, str>> {
|
||||||
const HOOK_PWD: &str = r#"
|
const HOOK_PWD: &str = r#"
|
||||||
function _zoxide_hook --on-variable PWD
|
function _zoxide_hook --on-variable PWD
|
||||||
zoxide add
|
zoxide add "$(pwd -L)"
|
||||||
end
|
end
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ alias {0}r='zoxide remove'
|
||||||
|
|
||||||
const HOOK_PROMPT: &str = r#"
|
const HOOK_PROMPT: &str = r#"
|
||||||
_zoxide_hook() {
|
_zoxide_hook() {
|
||||||
zoxide add
|
zoxide add "$(pwd -L)"
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$PS1" in
|
case "$PS1" in
|
||||||
|
@ -117,7 +117,7 @@ _zoxide_setpwd
|
||||||
_zoxide_hook() {{
|
_zoxide_hook() {{
|
||||||
_ZO_OLDPWD="$(cat "$_ZO_PWD_PATH")"
|
_ZO_OLDPWD="$(cat "$_ZO_PWD_PATH")"
|
||||||
if [ -z "$_ZO_OLDPWD" ] || [ "$_ZO_OLDPWD" != "$PWD" ]; then
|
if [ -z "$_ZO_OLDPWD" ] || [ "$_ZO_OLDPWD" != "$PWD" ]; then
|
||||||
_zoxide_setpwd && zoxide add > /dev/null
|
_zoxide_setpwd && zoxide add "$(pwd -L)" > /dev/null
|
||||||
fi
|
fi
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ function {0}ri {{
|
||||||
const HOOK_PROMPT: &str = r#"
|
const HOOK_PROMPT: &str = r#"
|
||||||
$PreZoxidePrompt = $function:prompt
|
$PreZoxidePrompt = $function:prompt
|
||||||
function prompt {
|
function prompt {
|
||||||
$null = zoxide add
|
$null = zoxide add $(Get-Location)
|
||||||
& $PreZoxidePrompt
|
& $PreZoxidePrompt
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
@ -81,7 +81,7 @@ const fn hook_pwd() -> Result<Cow<'static, str>> {
|
||||||
const HOOK_PWD: &str = r#"
|
const HOOK_PWD: &str = r#"
|
||||||
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
||||||
$ExecutionContext.InvokeCommand.LocationChangedAction = {
|
$ExecutionContext.InvokeCommand.LocationChangedAction = {
|
||||||
$null = zoxide add
|
$null = zoxide add $(Get-Location)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Write-Error "pwd hook requires pwsh - use 'zoxide init powershell --hook prompt'"
|
Write-Error "pwd hook requires pwsh - use 'zoxide init powershell --hook prompt'"
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub const CONFIG: ShellConfig = ShellConfig {
|
||||||
|
|
||||||
const HOOK_PROMPT: &str = r#"
|
const HOOK_PROMPT: &str = r#"
|
||||||
_zoxide_hook() {
|
_zoxide_hook() {
|
||||||
zoxide add
|
zoxide add "$(pwd -L)"
|
||||||
}
|
}
|
||||||
|
|
||||||
[[ -n "${precmd_functions[(r)_zoxide_hook]}" ]] || {
|
[[ -n "${precmd_functions[(r)_zoxide_hook]}" ]] || {
|
||||||
|
@ -26,7 +26,7 @@ _zoxide_hook() {
|
||||||
const fn hook_pwd() -> Result<Cow<'static, str>> {
|
const fn hook_pwd() -> Result<Cow<'static, str>> {
|
||||||
const HOOK_PWD: &str = r#"
|
const HOOK_PWD: &str = r#"
|
||||||
_zoxide_hook() {
|
_zoxide_hook() {
|
||||||
zoxide add
|
zoxide add "$(pwd -L)"
|
||||||
}
|
}
|
||||||
|
|
||||||
chpwd_functions=(${chpwd_functions[@]} "_zoxide_hook")
|
chpwd_functions=(${chpwd_functions[@]} "_zoxide_hook")
|
||||||
|
|
|
@ -25,7 +25,7 @@ fn remove(path: &str) -> Result<()> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = util::canonicalize(&path)?;
|
let path = util::resolve_path(&path)?;
|
||||||
let path = util::path_to_str(&path)?;
|
let path = util::path_to_str(&path)?;
|
||||||
|
|
||||||
if let Some(idx) = db.dirs.iter().position(|dir| dir.path == path) {
|
if let Some(idx) = db.dirs.iter().position(|dir| dir.path == path) {
|
||||||
|
|
135
src/util.rs
135
src/util.rs
|
@ -1,9 +1,9 @@
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::db::{Db, Epoch};
|
use crate::db::{Db, Epoch};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
pub fn get_db() -> Result<Db> {
|
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()))
|
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(¤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::<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> {
|
pub fn path_to_str<P: AsRef<Path>>(path: &P) -> Result<&str> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
path.to_str()
|
path.to_str()
|
||||||
|
|
Loading…
Reference in New Issue