z.plugin.zsh
79281 bytes
1################################################################################
2# Zsh-z - jump around with Zsh - A native Zsh version of rupa/z without awk,
3# sort, date, or sed
4#
5# https://github.com/agkozak/zsh-z
6#
7# Copyright (c) 2018-2026 Alexandros Kozak
8#
9# Permission is hereby granted, free of charge, to any person obtaining a copy
10# of this software and associated documentation files (the "Software"), to deal
11# in the Software without restriction, including without limitation the rights
12# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13# copies of the Software, and to permit persons to whom the Software is
14# furnished to do so, subject to the following conditions:
15#
16# The above copyright notice and this permission notice shall be included in all
17# copies or substantial portions of the Software.
18#
19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25# SOFTWARE.
26#
27# Zsh-z maintains a jump-list of the directories you actually use.
28#
29# INSTALL:
30# * put something like this in your .zshrc:
31# source /path/to/zsh-z.plugin.zsh
32# * cd around for a while to build up the database
33#
34# USAGE:
35# * z foo cd to the most frecent directory matching foo
36# * z foo bar cd to the most frecent directory matching both foo and bar
37# (e.g. /foo/bat/bar/quux)
38# * z -r foo cd to the highest ranked directory matching foo
39# * z -t foo cd to most recently accessed directory matching foo
40# * z -l foo List matches instead of changing directories
41# * z -e foo Echo the best match without changing directories
42# * z -c foo Restrict matches to subdirectories of PWD
43# * z -x Remove a directory (default: PWD) from the database
44# * z -xR Remove a directory (default: PWD) and its subdirectories from
45# the database
46#
47# ENVIRONMENT VARIABLES:
48#
49# ZSHZ_CASE -> if `ignore', pattern matching is case-insensitive; if `smart',
50# pattern matching is case-insensitive only when the pattern is all
51# lowercase
52# ZSHZ_CD -> the directory-changing command that is used (default: builtin cd)
53# ZSHZ_CMD -> name of command (default: z)
54# ZSHZ_COMPLETION -> completion method (default: 'frecent'; 'legacy' for
55# alphabetic sorting)
56# ZSHZ_DATA -> name of datafile (default: ~/.z)
57# ZSHZ_DEBUG -> if set, turn on debugging aids: WARN_CREATE_GLOBAL while the
58# command runs and per-function warnings (functions -W) at load time
59# (default: unset)
60# ZSHZ_ECHO -> if 1, print the directory name after jumping to it (default: 0)
61# ZSHZ_EXCLUDE_DIRS -> array of directories to exclude from your database
62# (default: empty)
63# ZSHZ_KEEP_DIRS -> array of directories that should not be removed from the
64# database, even if they are not currently available (default: empty)
65# ZSHZ_LOCK_TIMEOUT -> seconds to wait for the lockfile before giving up
66# (default: 1)
67# ZSHZ_MAX_SCORE -> maximum combined score the database entries can have
68# before beginning to age (default: 9000)
69# ZSHZ_NO_RESOLVE_SYMLINKS -> '1' prevents symlink resolution
70# ZSHZ_OWNER -> your username (if you want use Zsh-z while using sudo -s)
71# ZSHZ_TILDE -> if 1, display ~ in place of the full $HOME path in output
72# (default: 0)
73# ZSHZ_TRAILING_SLASH -> if 1, a query ending in / matches at the end of a
74# directory path (default: 0)
75# ZSHZ_UNCOMMON -> if 1, do not jump to "common directories," but rather drop
76# subdirectories based on what the search string was (default: 0)
77################################################################################
78
79# Minimalistic solution to allow this plugin to keep running under sh/bash/ksh
80# emulation while continuing to use Zsh-only syntax features. `emulate zsh -c'
81# evaluates its argument as code, so the script's own path -- `${(%):-%N}' --
82# must be `${(q)}'-quoted; otherwise an install directory containing spaces or
83# other shell-special characters (common on Cygwin/MSYS2 and macOS, where a
84# home directory can be "C:\Users\John Smith" or "/Users/John Smith") would be
85# word-split and the plugin would silently fail to re-source.
86if [[ -o KSH_ARRAYS || -o SH_WORD_SPLIT ]]; then
87 emulate zsh -c "source ${(q)${(%):-%N}}"
88 return $?
89fi
90
91autoload -Uz is-at-least
92
93if ! is-at-least 4.3.11; then
94 print "Zsh-z requires Zsh v4.3.11 or higher." >&2
95 return 1 2> /dev/null || exit 1
96fi
97
98############################################################
99# The help message
100#
101# Globals:
102# ZSHZ_CMD
103############################################################
104_zshz_usage() {
105 print "Usage: ${ZSHZ_CMD:-${_Z_CMD:-z}} [OPTION]... [ARGUMENT]
106Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial string ARGUMENT.
107
108With no ARGUMENT, list the directory history in ascending rank.
109
110 --add Add a directory to the database
111 -c Only match subdirectories of the current directory
112 -e Echo the best match without going to it
113 -h Display this help and exit
114 -l List all matches without going to them
115 -r Match by rank
116 -t Match by recent access
117 -x Remove a directory from the database (by default, the current directory)
118 -xR Remove a directory and its subdirectories from the database (by default, the current directory)" |
119 fold -s -w $(( COLUMNS > 0 ? COLUMNS : 80 )) >&2
120}
121
122############################################################
123# Canonicalize a path in the manner of `:A' -- normalize it
124# lexically as `:a' does, then resolve symlinks -- without
125# requiring any of the path to exist.
126#
127# `${x:A}' itself cannot be trusted with a missing path on
128# Zsh 4.3.11: when the top-level component of $x does not
129# exist (`/gone/sub'), the realpath machinery segfaults the
130# shell (upstream bug, 4.3.11 only; deeper missing
131# components are handled correctly on every version). So
132# apply `:A' only to the deepest ancestor of the path that
133# exists -- `:A' on an existing path is safe everywhere --
134# and reattach the missing components verbatim. That
135# reproduces `:A' exactly: `:A' resolves the symlinks in the
136# existing prefix and carries the nonexistent tail
137# unchanged, and the tail cannot contain live symlinks
138# precisely because it does not exist. (A broken symlink
139# stops the ancestor walk without being resolved -- `-e'
140# fails on one -- which also matches `:A', which leaves
141# broken symlinks unresolved.)
142#
143# Arguments:
144# $1 The path to canonicalize
145#
146# Returns the canonical path in $REPLY.
147############################################################
148_zshz_realpath() {
149 local dir=${1:a}
150 local -a tail
151
152 # `:h' at its fixed point (`/', or `//' where the OS treats that as
153 # distinct) can climb no higher; if even that much of the path does not
154 # exist, settle for the lexical normalization rather than hand `:A'
155 # something dangerous.
156 while [[ ! -e $dir && $dir != "${dir:h}" ]]; do
157 tail=( "${dir:t}" "${tail[@]}" )
158 dir=${dir:h}
159 done
160 [[ -e $dir ]] && dir=${dir:A}
161
162 # `typeset -g': REPLY belongs to the caller by design. A plain assignment
163 # would trip WARN_NESTED_VAR under `ZSHZ_DEBUG', since _zshz_realpath is a
164 # top-level function and thus one of the ones `functions -W' marks.
165 if (( ${#tail} )); then
166 typeset -g REPLY=${dir%/}/${(j:/:)tail}
167 else
168 typeset -g REPLY=$dir
169 fi
170}
171
172# Load zsh/datetime module, if necessary
173(( ${+EPOCHSECONDS} )) || zmodload zsh/datetime
174
175# Global associative array for internal use
176typeset -gA ZSHZ
177
178# Fallback utilities in case Zsh lacks zsh/files (as is the case with MobaXterm)
179ZSHZ[CHMOD]='chmod'
180ZSHZ[CHOWN]='chown'
181ZSHZ[MV]='mv'
182ZSHZ[RM]='rm'
183
184# Try to load zsh/files. zf_chown, zf_mv, and zf_rm are usually present in Zsh
185# 4.3.11. zf_chmod only became available in Zsh 5.0, so we load it separately
186# below. If zsh/files is not available at all, we silently fall back to the
187# external utilities chmod, chown, mv, and rm.
188if [[ ${builtins[zf_chown]-} != 'defined' ||
189 ${builtins[zf_mv]-} != 'defined' ||
190 ${builtins[zf_rm]-} != 'defined' ]]; then
191 zmodload -F zsh/files b:zf_chown b:zf_mv b:zf_rm &> /dev/null
192fi
193
194[[ ${builtins[zf_chmod]-} == 'defined' ]] ||
195 zmodload -F zsh/files b:zf_chmod &> /dev/null
196
197# Use zsh/files, if it is available.
198[[ ${builtins[zf_chmod]-} == 'defined' ]] && ZSHZ[CHMOD]='zf_chmod'
199[[ ${builtins[zf_chown]-} == 'defined' ]] && ZSHZ[CHOWN]='zf_chown'
200[[ ${builtins[zf_mv]-} == 'defined' ]] && ZSHZ[MV]='zf_mv'
201[[ ${builtins[zf_rm]-} == 'defined' ]] && ZSHZ[RM]='zf_rm'
202
203# Load zsh/system, if necessary
204[[ ${modules[zsh/system]-} == 'loaded' ]] || zmodload zsh/system &> /dev/null
205
206# Make sure ZSHZ_EXCLUDE_DIRS has been declared so that other scripts can
207# simply append to it
208(( ${+ZSHZ_EXCLUDE_DIRS} )) || typeset -gUa ZSHZ_EXCLUDE_DIRS
209
210# Determine if zsystem flock is available
211zsystem supports flock &> /dev/null && ZSHZ[USE_FLOCK]=1
212
213# Windows only: how many times to retry a datafile rename that fails.
214#
215# On Cygwin and MSYS2, rename() fails with EBUSY or EACCES whenever another
216# process holds the tempfile or the datafile open without FILE_SHARE_DELETE --
217# which is precisely what a virus scanner or the search indexer does to a file
218# in the moments after it is created. Since the write path below creates the
219# tempfile and renames it over the datafile microseconds later, that window is
220# wide open. The rename's stderr is discarded there, so a scan that lands in
221# the window silently loses an `--add' or a `-x': no message, no delay, just a
222# directory that never made it into the database. The condition clears in
223# milliseconds, so make a few more attempts before giving up.
224#
225# Everywhere else a failed rename means something real -- ENOSPC, EPERM, a
226# cross-device move -- that retrying cannot fix and would only add latency to,
227# so ZSHZ[MV_RETRIES] stays unset and the loops below make a single attempt,
228# exactly as before.
229#
230# zsh/zselect provides the sub-second delay between attempts without forking
231# /bin/sleep, whose fractional-seconds support is not portable in any case.
232# MobaXterm's cut-down Cygwin does not ship zsh/zselect, so there
233# ZSHZ[MV_RETRY_DELAY] stays unset and the retries happen back to back -- still
234# worth making, since the scanner's handle is often gone by the next attempt.
235#
236# Four retries at 50ms is deliberately modest rather than generous. The rename
237# runs while the lockfile is held, so every millisecond spent retrying is a
238# millisecond other writers spend waiting, and they give up after
239# ZSHZ_LOCK_TIMEOUT (1s by default) -- silently, since their adds are
240# best-effort too. A budget that outlasts a large fraction of that timeout
241# would trade one process's lost write for several others'. Measured on MSYS2
242# against a handle held open with FILE_SHARE_READ, this recovers renames
243# blocked for up to ~0.3s, comfortably more than a scan of a file this small
244# takes.
245if [[ $OSTYPE == (cygwin|msys) ]]; then
246 ZSHZ[MV_RETRIES]=4
247 [[ ${modules[zsh/zselect]-} == 'loaded' ]] || zmodload zsh/zselect &> /dev/null
248 # In hundredths of a second, per `zselect -t'
249 [[ ${builtins[zselect]-} == 'defined' ]] && ZSHZ[MV_RETRY_DELAY]=5
250fi
251
252############################################################
253# The Zsh-z Command
254#
255# Globals:
256# ZSHZ
257# ZSHZ_CASE
258# ZSHZ_CD
259# ZSHZ_COMPLETION
260# ZSHZ_DATA
261# ZSHZ_DEBUG
262# ZSHZ_EXCLUDE_DIRS
263# ZSHZ_KEEP_DIRS
264# ZSHZ_LOCK_TIMEOUT
265# ZSHZ_MAX_SCORE
266# ZSHZ_OWNER
267#
268# Arguments:
269# $* Command options and arguments
270############################################################
271zshz() {
272
273 # Don't use `emulate -L zsh' - it breaks PUSHD_IGNORE_DUPS
274 setopt LOCAL_OPTIONS NO_KSH_ARRAYS NO_SH_WORD_SPLIT EXTENDED_GLOB UNSET
275 (( ZSHZ_DEBUG )) && setopt LOCAL_OPTIONS WARN_CREATE_GLOBAL
276
277 local REPLY
278 local -a lines
279
280 # Allow the user to specify a custom datafile in $ZSHZ_DATA (or legacy $_Z_DATA)
281 local custom_datafile="${ZSHZ_DATA:-$_Z_DATA}"
282
283 # $_zshz_quiet_add marks the automatic bookkeeping add that _zshz_precmd
284 # runs in a `&!' fork before every prompt (_zshz_precmd declares it `local',
285 # so it is visible here only through that one call). A fork cannot
286 # record anything in the parent shell, so it has no way to warn just once:
287 # an unusable $ZSHZ_DATA would otherwise put the same diagnostic on the
288 # terminal at every prompt for the life of the shell. Stay quiet on that
289 # path and leave the complaining to the entry points the user actually
290 # invoked -- including a hand-typed `z --add', which is not marked and so
291 # still reports.
292 local quiet
293 [[ -n ${_zshz_quiet_add-} ]] && quiet=1
294
295 # If a datafile was provided as a standalone file without a directory path
296 # print a warning and return
297 if [[ -n ${custom_datafile} && ${custom_datafile} != */* ]]; then
298 (( quiet )) ||
299 print "ERROR: You configured a custom Zsh-z datafile (${custom_datafile}), but have not specified its directory." >&2
300 return 1
301 fi
302
303 # Refuse a symlinked datafile while $ZSHZ_OWNER is set, rather than
304 # following it. That variable means root is acting for an unprivileged user
305 # -- the documented `sudo -s' setup -- and the resolution just below
306 # deliberately dereferences a link, so in that configuration Zsh-z would
307 # write the database wherever a name inside the user's own home points, with
308 # root's authority. Nothing has to be raced: the link is planted before the
309 # privileged shell ever starts. Unprivileged use crosses no such boundary and
310 # keeps the dereference, which is what makes pointing `.z' at synced storage
311 # work.
312 #
313 # Every component, not just the last. Resolution walks the whole path, so a
314 # symlinked *parent* redirects it just as effectively: with `link' -> `/etc'
315 # inside a user's home, a datafile of `~/link/passwd' resolves to
316 # `/etc/passwd' and root rewrites it.
317 #
318 # Judged by who owns each link rather than by its mere presence. Symlinked
319 # system directories are ordinary -- `/home' -> `/usr/home' on the BSDs,
320 # `/var' -> `/private/var' on macOS -- and refusing those would break Zsh-z
321 # under $ZSHZ_OWNER on those systems for nothing. Those are root's; what this
322 # has to reject is a link an unprivileged owner could have planted. `zstat
323 # -L' reports the link's own owner rather than its target's, which is the
324 # distinction `-O' cannot make.
325 if [[ -n ${ZSHZ_OWNER:-${_Z_OWNER}} ]]; then
326 local _zshz_df=${custom_datafile:-$HOME/.z}
327 [[ $_zshz_df == /* ]] || _zshz_df="$PWD/$_zshz_df"
328 zmodload -F zsh/stat b:zstat 2> /dev/null
329 local _zshz_pfx _zshz_part _zshz_luid
330 for _zshz_part in ${(s:/:)_zshz_df}; do
331 [[ -n $_zshz_part ]] || continue
332 _zshz_pfx+="/$_zshz_part"
333 [[ -L $_zshz_pfx ]] || continue
334 # Without zsh/stat there is no way to tell whose link this is, so refuse
335 # it rather than guess: this path is privileged by definition.
336 _zshz_luid=''
337 (( ${+builtins[zstat]} )) &&
338 _zshz_luid=$(zstat -L +uid "$_zshz_pfx" 2> /dev/null)
339 if [[ $_zshz_luid != 0 ]]; then
340 (( quiet )) ||
341 print "ERROR: Zsh-z will not follow the symlink ${_zshz_pfx} on the way to its datafile while ZSHZ_OWNER is set." >&2
342 return 1
343 fi
344 done
345 fi
346
347 # If the user specified a datafile, use that or default to ~/.z
348 # If the datafile is a symlink, it gets dereferenced (except under
349 # $ZSHZ_OWNER, refused just above). Canonicalized with
350 # _zshz_realpath rather than a bare `:A', which would segfault Zsh 4.3.11
351 # on a $ZSHZ_DATA pointing into a missing top-level directory -- at every
352 # prompt, since this line runs in the backgrounded precmd add.
353 _zshz_realpath "${custom_datafile:-$HOME/.z}"
354 local datafile=$REPLY
355 # Clear REPLY as soon as it is captured: the matching machinery below
356 # relies on it staying empty until a common root or best match is put in
357 # it (_zshz_find_common_root only assigns REPLY when it finds a root), so
358 # a datafile path left in REPLY here would surface as a bogus match.
359 REPLY=''
360
361 # If the datafile is a directory, print a warning and return
362 if [[ -d $datafile ]]; then
363 (( quiet )) ||
364 print "ERROR: Zsh-z's datafile (${datafile}) is a directory." >&2
365 return 1
366 fi
367
368 # Make sure that the datafile exists before attempting to read it or lock it
369 # for writing. Create it with 0600 permissions from the first instant (umask
370 # in a subshell) rather than chmodding it afterward: this creation runs
371 # before the lock is taken, and on Cygwin/MSYS2 a concurrent writer's rename
372 # passes through a window in which the datafile is unlinked or delete-
373 # pending, so any second syscall on the path (chmod) -- or even the creating
374 # open itself -- can fail spuriously. Append mode (>>) creates the file
375 # without truncating one that a concurrent writer has just renamed into
376 # place. The first attempt is silent; if the file still does not exist
377 # afterward (so no concurrent writer supplied it), retry loudly so that real
378 # failures (directory permissions, read-only filesystem) reach the user.
379 [[ -f $datafile ]] || {
380 mkdir -p "${datafile:h}" &&
381 ( umask 077; : >> "$datafile" ) 2> /dev/null ||
382 [[ -f $datafile ]] ||
383 ( umask 077; : >> "$datafile" )
384 # When $ZSHZ_OWNER is set (e.g. under `sudo -s'), hand the freshly created
385 # file off to that user immediately, so a query-only invocation can't leave
386 # behind a root-owned .z that the normal-user shell can't read. `-h' so a
387 # symlink that appeared since the check above is retitled itself rather
388 # than dereferenced onto its target.
389 local _owner=${ZSHZ_OWNER:-${_Z_OWNER}}
390 [[ -n $_owner ]] &&
391 ${ZSHZ[CHOWN]} -h "${_owner}:$(id -ng "${_owner}")" "$datafile"
392 }
393
394 # If the datafile still does not exist, the loud retry above has already
395 # said why; nothing below -- reading, locking, writing -- can succeed
396 # without it, and each failure would add its own noise. Bailing out here
397 # matters most on Zsh 4.3.11, where the failed `$(< $datafile)' reads
398 # below are fatal to a non-interactive shell.
399 [[ -f $datafile ]] || return 1
400
401 # Bail if we don't own the datafile and $ZSHZ_OWNER is not set
402 [[ -z ${ZSHZ_OWNER:-${_Z_OWNER}} && -f $datafile && ! -O $datafile ]] &&
403 return
404
405 ############################################################
406 # Add a path to or remove one from the datafile
407 #
408 # Globals:
409 # ZSHZ
410 # ZSHZ_EXCLUDE_DIRS
411 # ZSHZ_LOCK_TIMEOUT
412 # ZSHZ_NO_RESOLVE_SYMLINKS
413 # ZSHZ_OWNER
414 #
415 # Arguments:
416 # $1 Which action to perform (--add/--remove)
417 # $2 The path to add
418 ############################################################
419 _zshz_add_or_remove_path() {
420 local action=$1
421 shift
422
423 if [[ $action == '--add' ]]; then
424
425 # These $HOME / $ZSHZ_EXCLUDE_DIRS guards mirror the ones in
426 # _zshz_precmd, but they are not redundant: precmd filters $PWD as an
427 # early-out (skip the background fork), whereas --add is now a public
428 # entry point and must enforce the same policies as the precmd function.
429 # Keep both in sync.
430
431 # Don't add $HOME
432 [[ $* == $HOME ]] && return
433
434 # Don't track directory trees excluded in $ZSHZ_EXCLUDE_DIRS
435 local exclude
436 for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
437 case $* in
438 ${exclude}|${exclude}/*) return ;;
439 esac
440 done
441 fi
442
443 # Resolve the directory to be removed, and confirm a full-database wipe,
444 # *before* taking the lock. Both are independent of the datafile, and the
445 # confirmation is interactive: holding the lock across a `read -q' the user
446 # might walk away from would make concurrent writers in other shells time
447 # out on ZSHZ_LOCK_TIMEOUT and silently drop their adds while the prompt
448 # sits open. A lock should wrap the read-modify-write, never a question.
449 local xdir # Directory to be removed
450 if [[ $action == '--remove' ]]; then
451 # The target is canonicalized without any existence test: an entry
452 # whose directory has since been deleted is exactly the one a user most
453 # wants out of the database. _zshz_realpath resolves a missing path the
454 # way `:A' resolves one -- and, unlike a bare `:A', cannot segfault Zsh
455 # 4.3.11 on a path whose top-level component is gone. (The old
456 # `[[ -d ${...:A} ]]' guard offered no protection there: the `:A'
457 # expands, and crashes, before `-d' ever sees it.)
458 if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
459 xdir=${${*:-${PWD}}:a}
460 else
461 _zshz_realpath "${*:-${PWD}}"
462 xdir=$REPLY
463 fi
464
465 # Both branches above yield a non-empty absolute path, and that
466 # matters: under `-R' an empty $xdir would collapse the subtree filter
467 # below into `${lines_to_keep:#/**}', which matches every line in the
468 # datafile and erases the lot -- silently, since the whole-database
469 # confirmation just below tests for `/' rather than for emptiness. Keep
470 # this guard in case a future change lets an empty resolution through.
471 [[ -n $xdir ]] || return 1
472
473 if (( ${+opts[-R]} )) && [[ $xdir == '/' ]]; then
474 if ! read -q "?Delete entire Zsh-z database? "; then
475 print && return 1
476 fi
477 fi
478 fi
479
480 # A temporary file that gets copied over the datafile if all goes well
481 local tempfile="${datafile}.${RANDOM}" lockfile="${datafile}.lock"
482 integer lockfd=0
483 # The no-flock fallback's lock. Deliberately a *different* name from
484 # $lockfile: a plain file left behind by a flock-capable Zsh would make
485 # `mkdir' fail forever on the same path, deadlocking every later write.
486 local lockdir="${datafile}.lock.d"
487 integer lockdir_held=0
488
489 {
490 # Using zsystem flock
491 if (( ZSHZ[USE_FLOCK] )); then
492
493 # Obtain an exclusive lock on the lockfile.
494 #
495 # Locking the datafile directly would not actually serialize concurrent
496 # writers, since the datafile gets replaced by mv and each new datafile
497 # has a new inode -- so a separate, stable lockfile is needed.
498 #
499 # Bound the lock acquisition (default 1s, override with ZSHZ_LOCK_TIMEOUT)
500 # so a stuck holder can't stall the backgrounded precmd add or freeze a
501 # user's foreground `z --add' / `z -x'. Once the holder dies, the kernel
502 # frees the lock and the next add succeeds automatically -- no manual
503 # `rm ~/.z.lock' needed.
504 #
505 # On timeout we return silently and on purpose: the precmd add is
506 # best-effort and runs backgrounded (`&!'), so there is nowhere useful
507 # to report to -- a message would land on the terminal asynchronously,
508 # mid-keystroke, possibly every prompt. To diagnose a database that has
509 # stopped updating, run a foreground `z --add .' and check `$?': a
510 # nonzero status means the write did not happen -- 2 is a lock-
511 # acquisition timeout (contention, or a raised ZSHZ_LOCK_TIMEOUT is
512 # still too low), 1 is a permissions or ownership problem (e.g. a stale
513 # root-owned lockfile left by an earlier `sudo -s' session, or a
514 # symlinked lockfile refused under $ZSHZ_OWNER).
515 # Create the lockfile 0600-from-birth and silently (umask in a
516 # subshell), mirroring the datafile creation above rather than a bare
517 # `touch' under the ambient umask with unsuppressed stderr. zsystem
518 # flock opens the lockfile O_RDWR, so under `sudo -s' with $ZSHZ_OWNER
519 # the unprivileged user must be able to open it: hand it off at
520 # creation, not only after a successful write -- a timed-out or failed
521 # first write by root would skip the post-write chown and leave a
522 # root-owned lockfile, turning every later user --add / -x into a
523 # silently-swallowed EACCES no-op. The lockfile is deliberately never
524 # removed: unlinking one a waiter has already opened reintroduces the
525 # two-inodes race the stable lockfile exists to prevent.
526 # Under $ZSHZ_OWNER all of this runs with root's authority on a path the
527 # unprivileged owner controls, and every step follows a symlink: `-f'
528 # tests the target, `>>' creates a dangling one, and flock opens it.
529 # $datafile survives a planted link only because the `mv' below replaces
530 # it outright; the lockfile is deliberately never removed, so a symlink
531 # here would persist and be acted on at every subsequent write. Refuse.
532 local _lock_owner=${ZSHZ_OWNER:-${_Z_OWNER}}
533 [[ -n $_lock_owner && -L $lockfile ]] && return 1
534 if [[ ! -f $lockfile ]]; then
535 ( umask 077; : >> "$lockfile" ) 2> /dev/null
536 [[ -n $_lock_owner ]] &&
537 ${ZSHZ[CHOWN]} -h "${_lock_owner}:$(id -ng "${_lock_owner}")" "$lockfile"
538 fi
539 zsystem flock -t ${ZSHZ_LOCK_TIMEOUT:-1} -f lockfd "$lockfile" 2> /dev/null || return
540
541 else
542
543 # No `zsystem flock' here. MobaXterm's cut-down Cygwin is the case that
544 # matters -- it ships no `zsh/system' at all -- and until now this path
545 # wrote with nothing serializing it: every writer read its own snapshot
546 # and the last `mv' won. Measured on MobaXterm, an entry added by one of
547 # four concurrent writers went missing in 7 runs out of 10.
548 #
549 # `mkdir' is the portable atomic primitive: it succeeds for exactly one
550 # caller and fails for the rest, with no module behind it. What it does
551 # not give us is the kernel's release-on-death, which is the whole
552 # reason `flock' is preferred where it exists -- so a holder that dies
553 # would wedge every later write. Hence the staleness sweep below.
554 #
555 # Failure to acquire returns 2, the same status the flock branch's
556 # timeout produces and the one the README documents for contention.
557 integer _zshz_deadline=$(( EPOCHSECONDS + ${ZSHZ_LOCK_TIMEOUT:-1} ))
558 local -a _zshz_stale
559 while :; do
560 if mkdir "$lockdir" 2> /dev/null; then
561 lockdir_held=1
562 break
563 fi
564 # Break a lock nobody can still be holding. A write is a matter of
565 # milliseconds, so a lock directory older than 30 seconds means its
566 # owner died without releasing it. `mkdir' stamps the mtime at
567 # creation and no holder touches it afterwards, so the age is the
568 # hold time. `$lockdir' expands literally here -- only the qualifier
569 # is glob syntax -- so a datafile path containing `[' or `*' is safe.
570 _zshz_stale=( ${lockdir}(Nms+30) )
571 if (( ${#_zshz_stale} )); then
572 rmdir "$lockdir" 2> /dev/null && continue
573 fi
574 (( EPOCHSECONDS >= _zshz_deadline )) && return 2
575 # No `zselect' on the platforms that land here, so this costs a fork.
576 # It is the slow path already, and spinning would be worse.
577 sleep 0.05 2> /dev/null || :
578 done
579
580 fi
581
582 # Read the datafile only after obtaining the lock, so concurrent --add
583 # calls don't all act on the same stale snapshot.
584 lines=( ${(f)"$(< $datafile)"} )
585 # Discard entries that are incomplete or incorrectly formatted
586 lines=( ${(M)lines:#/*\|[[:digit:]]##[.,]#[[:digit:]]#\|[[:digit:]]##} )
587
588 # Hold the fd in an *unset* scalar, not `integer tmpfd' (which seeds it
589 # with 0). On some Zsh builds, `exec {tmpfd}>|...' refuses to clobber a
590 # parameter already holding a number that names an open fd -- and 0 is
591 # stdin, always open -- yielding "can't clobber parameter tmpfd
592 # containing file descriptor 0". An empty scalar isn't a valid fd, so
593 # the guard never fires. See https://github.com/agkozak/zsh-z/issues/81
594 local tmpfd
595 case $action in
596 --add)
597 # When zf_chmod isn't available (Zsh 4.3.11), avoid the
598 # ~900us fork+execve of external /usr/bin/chmod on every
599 # write. Create the tempfile with mode 0600 from the start
600 # via `umask 077' inside a subshell -- the umask change is
601 # contained to the forked child process and the OS prevents
602 # it from leaking back to the parent. Subshell fork without
603 # exec is ~50us, ~18x cheaper than the chmod fallback.
604 if [[ ${ZSHZ[CHMOD]} == 'zf_chmod' ]]; then
605 exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
606 # Fail closed. The tempfile is born with the ambient umask (0666
607 # under `umask 000'), and it is this inode -- not the datafile's --
608 # that the rename below publishes, so a chmod whose failure went
609 # unnoticed would replace a 0600 datafile with a world-readable one
610 # and still report success. Nothing has been written yet, so
611 # there is no salvage: drop the tempfile and leave the database as
612 # it was.
613 if ! ${ZSHZ[CHMOD]} 600 "$tempfile"; then
614 exec {tmpfd}>&-
615 ${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
616 return 1
617 fi
618 _zshz_update_datafile $tmpfd "$*"
619 else
620 ( umask 077
621 exec {tmpfd}>|"$tempfile"
622 _zshz_update_datafile $tmpfd "$*" )
623 fi
624 local ret=$?
625 ;;
626 --remove)
627 # $xdir was resolved before the lock, and for `-xR /' the
628 # whole-database wipe was already confirmed there.
629 local -a lines_to_keep
630 if (( ${+opts[-R]} )); then
631 # All of the lines that don't match the directory to be deleted
632 lines_to_keep=( ${lines:#${xdir}\|*} )
633 # Or its subdirectories
634 lines_to_keep=( ${lines_to_keep:#${xdir%/}/**} )
635 else
636 # All of the lines that don't match the directory to be deleted
637 lines_to_keep=( ${lines:#${xdir}\|*} )
638 fi
639 if [[ $lines != "$lines_to_keep" ]]; then
640 lines=( $lines_to_keep )
641 else
642 return 1 # The $PWD isn't in the datafile
643 fi
644 # Same umask-subshell pattern as --add: avoid the external
645 # chmod when zf_chmod isn't available.
646 if [[ ${ZSHZ[CHMOD]} == 'zf_chmod' ]]; then
647 exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
648 # Fail closed, exactly as on the --add path above.
649 if ! ${ZSHZ[CHMOD]} 600 "$tempfile"; then
650 exec {tmpfd}>&-
651 ${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
652 return 1
653 fi
654 # `-r': $lines are verbatim on-disk lines (the datafile stores
655 # literal paths), so they must be written back unchanged. Without
656 # `-r', print would collapse an escape -- e.g. a literal `\t' in a
657 # path into a tab -- silently corrupting bystander entries.
658 print -u $tmpfd -rl -- $lines
659 else
660 ( umask 077; print -rl -- $lines >| "$tempfile" )
661 fi
662 local ret=$?
663 ;;
664 esac
665
666 if [[ -n $tmpfd ]]; then
667 # Close tempfile
668 exec {tmpfd}>&-
669 fi
670
671 if (( ret != 0 )); then
672 # Avoid clobbering the datafile if the write to tempfile failed
673 ${ZSHZ[RM]} -f "$tempfile"
674 return $ret
675 fi
676
677 integer write_ret chown_ret mv_attempts
678 local owner
679 owner=${ZSHZ_OWNER:-${_Z_OWNER}}
680
681 if (( ZSHZ[USE_FLOCK] )); then
682 # An unusual case: if inside Docker container where datafile could be bind
683 # mounted
684 if [[ -f '/.dockerenv' || ( -r '/proc/1/cgroup' && "$(< '/proc/1/cgroup')" == *docker* ) ]]; then
685 # Secure the datafile *before* its contents land. This branch writes
686 # in place instead of renaming an already-0600 tempfile over the
687 # path, so asserting the mode afterwards -- as this did -- leaves a
688 # bind-mounted datafile that arrived permissive readable for the
689 # length of the write, and leaves it readable for good if the chmod
690 # fails and nothing checks. The mode carries across the truncating
691 # write below, which reuses this same inode.
692 if ! ${ZSHZ[CHMOD]} 600 "$datafile" 2> /dev/null; then
693 ${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
694 return 1
695 fi
696 # This is the one write path where a symlink at $datafile redirects
697 # real database content: the sibling branch renames a finished
698 # tempfile over the path, and a rename *replaces* a link rather than
699 # writing through it, while `>|' follows one. Under $ZSHZ_OWNER that
700 # content goes out with root's authority to a path an unprivileged
701 # owner controls, so a `-L' test ahead of the write is not enough --
702 # the path can be swapped in between.
703 #
704 # `sysopen -o nofollow' settles it atomically, at open time, and the
705 # write goes through that descriptor. If it is unavailable (Zsh
706 # 4.3.11 has `zsystem flock' but no `sysopen' at all, and O_NOFOLLOW
707 # is not universal) or it refuses the open, the privileged write is
708 # refused rather than retried by a following one: this degrades to
709 # failing closed, never to writing unsafely. Without an owner set no
710 # privilege is crossed and the plain redirection stands.
711 #
712 # `chmod' above stays path-based -- Zsh has no `fchmod' -- so a swap
713 # can still misdirect it. Setting the mode on the wrong file is a far
714 # smaller matter than writing the database into it, and the write is
715 # what this closes.
716 local _zshz_dfd
717 if [[ -n $owner ]]; then
718 if (( ${+builtins[sysopen]} )) &&
719 sysopen -o trunc,nofollow -w -u _zshz_dfd "$datafile" 2> /dev/null
720 then
721 print -u $_zshz_dfd -r -- "$(< "$tempfile")" 2> /dev/null
722 write_ret=$?
723 exec {_zshz_dfd}>&-
724 else
725 ${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
726 return 1
727 fi
728 else
729 # `-r': re-emit the tempfile's already-literal contents byte-for-byte.
730 print -r -- "$(< "$tempfile")" >| "$datafile" 2> /dev/null
731 write_ret=$?
732 fi
733 ${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
734 # All other cases
735 else
736 # Retry a rename that a Windows sharing violation turned away; see
737 # the ZSHZ[MV_RETRIES] comment at the top of this file. Off Windows
738 # this loop makes the same single attempt it always has. Retrying is
739 # safe here: the rename happens under the lock, so no other writer
740 # can slip in between attempts.
741 while :; do
742 if ${ZSHZ[MV]} "$tempfile" "$datafile" 2> /dev/null; then
743 write_ret=0
744 else
745 write_ret=$?
746 fi
747 (( write_ret == 0 )) && break
748 (( mv_attempts++ >= ${ZSHZ[MV_RETRIES]:-0} )) && break
749 if (( ${+ZSHZ[MV_RETRY_DELAY]} )); then
750 zselect -t ${ZSHZ[MV_RETRY_DELAY]} || :
751 fi
752 done
753 (( write_ret != 0 )) && ${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
754 fi
755 # Preserve the write failure itself; best-effort tempfile cleanup must not
756 # turn a failed persist into a successful return.
757 (( write_ret == 0 )) || return $write_ret
758
759 if [[ -n $owner ]]; then
760 # Chown the lockfile alongside the datafile: zsystem flock opens it
761 # O_RDWR, so if root creates it first under sudo -s, the unprivileged
762 # $ZSHZ_OWNER user's flock attempts would fail with EACCES (silently
763 # swallowed), turning --add and -x into no-ops.
764 # `-h' on both: the lockfile is never replaced, so a symlink planted
765 # there outlives any one write, and $datafile can be relinked in the
766 # window between the `mv' above and this line. Retitling the link
767 # itself -- which the owner already owns -- costs nothing, while
768 # dereferencing hands root's authority to whatever it names.
769 ${ZSHZ[CHOWN]} -h "${owner}:$(id -ng "${owner}")" "$datafile" "$lockfile"
770 chown_ret=$?
771 # Surface post-write chown failures too: the current write landed, but a
772 # wrong owner can break the next locked write.
773 (( chown_ret == 0 )) || return $chown_ret
774 fi
775 else
776 if [[ -n $owner ]]; then
777 ${ZSHZ[CHOWN]} -h "${owner}:$(id -ng "${owner}")" "$tempfile"
778 chown_ret=$?
779 if (( chown_ret != 0 )); then
780 # In the no-flock path, chown happens before the move, so clean up the
781 # tempfile and leave the live database untouched.
782 ${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
783 return $chown_ret
784 fi
785 fi
786 # Same Windows sharing-violation retry as the flock branch above. This
787 # path is the one MobaXterm's cut-down Cygwin takes, and it has neither
788 # zsystem flock nor zsh/zselect, so the retries there run back to back.
789 while :; do
790 if ${ZSHZ[MV]} -f "$tempfile" "$datafile" 2> /dev/null; then
791 write_ret=0
792 else
793 write_ret=$?
794 fi
795 (( write_ret == 0 )) && break
796 (( mv_attempts++ >= ${ZSHZ[MV_RETRIES]:-0} )) && break
797 if (( ${+ZSHZ[MV_RETRY_DELAY]} )); then
798 zselect -t ${ZSHZ[MV_RETRY_DELAY]} || :
799 fi
800 done
801 if (( write_ret != 0 )); then
802 ${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
803 return $write_ret
804 fi
805 fi
806 } always {
807 # zsystem flock -f opens a real fd; explicitly unlock it so repeated
808 # foreground `z --add' / `z -x' invocations in the interactive shell
809 # don't leak lock descriptors and stall peers. (A backgrounded precmd
810 # child releases its fd on exit regardless; this matters for the parent.)
811 (( lockfd != 0 )) && zsystem flock -u $lockfd 2> /dev/null
812 # Release the mkdir lock on every exit from the block above, including
813 # the early `return's -- unlike an fd, a directory outlives the process
814 # that made it, so a missed release here is a wedged database rather than
815 # a leaked descriptor. Only if this call is the one that took it.
816 (( lockdir_held )) && rmdir "$lockdir" 2> /dev/null
817 }
818
819 # In order to make z -x work, we have to disable zsh-z's adding
820 # to the database until the user changes directory and the
821 # chpwd_functions are run
822 if [[ $action == '--remove' ]]; then
823 ZSHZ[DIRECTORY_REMOVED]=1
824 fi
825 }
826
827 ############################################################
828 # Read the current datafile contents, update them, "age" them
829 # when the total rank gets high enough, and print the new
830 # contents to STDOUT.
831 #
832 # Globals:
833 # ZSHZ_KEEP_DIRS
834 # ZSHZ_MAX_SCORE
835 #
836 # Arguments:
837 # $1 File descriptor linked to tempfile
838 # $2 Path to be added to datafile
839 ############################################################
840 _zshz_update_datafile() {
841
842 integer fd=$1
843 local -A rank time
844
845 # Characters special to the shell (such as '[]') are quoted with backslashes
846 # See https://github.com/rupa/z/issues/246
847 local add_path=${(q)2}
848
849 local now=$EPOCHSECONDS line dir
850 local path_field rank_field time_field count x
851 local -i keep
852
853 rank[$add_path]=1
854 time[$add_path]=$now
855
856 for line in $lines; do
857 path_field=${line%%\|*}
858
859 # Filter non-existent paths (honoring ZSHZ_KEEP_DIRS) inline so
860 # we walk $lines once instead of twice. The `keep=1; break' also
861 # fixes a latent bug: the previous existence-check loop had no
862 # `break' after appending, so a non-existent path matching
863 # multiple ZSHZ_KEEP_DIRS patterns was processed more than once.
864 if [[ ! -d $path_field ]]; then
865 keep=0
866 for dir in ${(@)ZSHZ_KEEP_DIRS}; do
867 if [[ $path_field == ${dir}/* || $path_field == $dir || $dir == '/' ]]; then
868 keep=1
869 break
870 fi
871 done
872 (( keep )) || continue
873 fi
874
875 # Quote in place: assoc-array keys need shell-special chars
876 # backslash-escaped (rupa/z#246).
877 path_field=${(q)path_field}
878 rank_field=${${line%\|*}#*\|}
879 time_field=${line##*\|}
880
881 # When a rank drops below 1, drop the path from the database
882 (( rank_field < 1 )) && continue
883
884 if [[ $path_field == $add_path ]]; then
885 # Compute the new rank with a scalar expression, not `(( rank[$key]++ ))'.
886 # The keys are `${(q)}'-quoted (rupa/z#246); a math-context subscript
887 # runs its key through the arithmetic lexer, which strips a backslash
888 # level and so misses any key containing `$ \ [ ] ( )' or a backtick --
889 # incrementing a phantom raw-keyed entry and leaving the real one stuck.
890 # An assignment subscript expands the key literally, so it is safe.
891 rank[$path_field]=$(( rank_field + 1 ))
892 time[$path_field]=$now
893 else
894 rank[$path_field]=$rank_field
895 time[$path_field]=$time_field
896 fi
897 (( count += rank_field ))
898 done
899 local -a out
900 if (( count > ${ZSHZ_MAX_SCORE:-${_Z_MAX_SCORE:-9000}} )); then
901 # Aging
902 for x in ${(k)rank}; do
903 # `${rank[$x]}', not a bare `rank[$x]' math subscript: the keys are
904 # `${(q)}'-quoted (rupa/z#246), and a math-context subscript would run
905 # the key through the arithmetic lexer, stripping a backslash level and
906 # missing any key with `$ \ [ ] ( )' or a backtick -- yielding 0, which
907 # the `rank_field < 1' drop above then erases on the next write. The
908 # expansion substitutes the numeric value before the math parser runs.
909 out+=( "$x|$(( 0.99 * ${rank[$x]} ))|${time[$x]}" )
910 done
911 else
912 for x in ${(k)rank}; do
913 out+=( "$x|${rank[$x]}|${time[$x]}" )
914 done
915 fi
916 # Deliberately NO `-r' here, unlike every other datafile write. The keys in
917 # $out are `${(q)}'-quoted (assoc-array keys need shell-special chars
918 # backslash-escaped -- rupa/z#246), and a plain `print' strips exactly one
919 # backslash level back off, so what lands on disk is the literal path the
920 # rest of the code expects. Adding `-r' would store the still-quoted form
921 # (e.g. `/foo\ bar'), which the read path -- it does not unquote -- would
922 # then fail to match. The verbatim-passthrough writes in
923 # `_zshz_add_or_remove_path' DO use `-r' because their input is already
924 # literal; this one is not.
925 print -u $fd -l -- $out || return 1
926 }
927
928 ############################################################
929 # The original tab completion method
930 #
931 # String processing is smartcase -- case-insensitive if the
932 # search string is lowercase, case-sensitive if there are
933 # any uppercase letters. Spaces in the search string are
934 # treated as *'s in globbing. Read the contents of the
935 # datafile and print matches to STDOUT.
936 #
937 # Arguments:
938 # $1 The string to be completed
939 ############################################################
940 _zshz_legacy_complete() {
941
942 local line path_field path_field_normalized
943
944 # Replace spaces in the search string with asterisks for globbing
945 1=${1//[[:space:]]/*}
946
947 # Hoist loop-invariants out of the per-line loop -- $1 and
948 # $ZSHZ_TRAILING_SLASH don't change inside the loop, so the
949 # lowercase comparison and the trailing-slash branch were pure
950 # waste when recomputed N times. `query_lower' lets the case-
951 # insensitive branch glob against a precompiled lowercase pattern.
952 local query_lower=${1:l}
953 local -i is_lowercase_query=0
954 [[ $1 == $query_lower ]] && is_lowercase_query=1
955 local -i trail=${ZSHZ_TRAILING_SLASH:-0}
956
957 for line in $lines; do
958
959 path_field=${line%%\|*}
960
961 path_field_normalized=$path_field
962 (( trail )) && path_field_normalized=${path_field%/}/
963
964 # If the search string is all lowercase, the search will be case-insensitive
965 if (( is_lowercase_query )) && [[ ${path_field_normalized:l} == *${~query_lower}* ]]; then
966 print -r -- $path_field
967 # Otherwise, case-sensitive
968 elif [[ $path_field_normalized == *${~1}* ]]; then
969 print -r -- $path_field
970 fi
971
972 done
973 # TODO: Search strings with spaces in them are currently treated case-
974 # insensitively.
975 }
976
977 ############################################################
978 # If matches share a common root, find it, and put it in
979 # REPLY for _zshz_output to use.
980 #
981 # Arguments:
982 # $@ Candidate paths
983 ############################################################
984 _zshz_find_common_root() {
985 local -a common_matches
986 local x short
987
988 common_matches=( "$@" )
989
990 for x in ${(@)common_matches}; do
991 if [[ -z $short ]] || (( $#x < $#short )) || [[ $x != ${short}/* ]]; then
992 short=$x
993 fi
994 done
995
996 [[ $short == '/' ]] && return
997
998 for x in ${(@)common_matches}; do
999 [[ $x != $short* ]] && return
1000 done
1001
1002 REPLY=$short
1003 }
1004
1005 ############################################################
1006 # Calculate a common root, if there is one. Then do one of
1007 # the following:
1008 #
1009 # 1) Print a list of completions in frecent order;
1010 # 2) List them (z -l) to STDOUT; or
1011 # 3) Put a common root or best match into REPLY
1012 #
1013 # Globals:
1014 # ZSHZ_TILDE
1015 # ZSHZ_UNCOMMON
1016 #
1017 # Arguments:
1018 # $1 Name of an associative array of matches and ranks
1019 # $2 The best match or best case-insensitive match
1020 # $3 Whether to produce a completion, a list, or a root or
1021 # match
1022 ############################################################
1023 _zshz_output() {
1024
1025 local match_array=$1 match=$2 format=$3
1026 local common x v
1027 local -a descending_list output
1028
1029 _zshz_find_common_root ${(@Pk)match_array}
1030 common=$REPLY
1031 # Clear REPLY once the common root is captured: the caller reads REPLY as
1032 # the jump target, so a value left over here would make `z -l <query>'
1033 # change directory after listing. The default arm below overwrites REPLY
1034 # deliberately; the completion and list arms must leave it empty.
1035 REPLY=''
1036
1037 # Iterate the caller's matches/imatches array as flat key-value
1038 # pairs via ${(@Pkv)...} instead of copying into a local
1039 # associative array. Avoids the hash-table allocation and K
1040 # inserts that the copy required.
1041 local -a kv
1042 local -i i
1043 kv=( ${(@Pkv)match_array} )
1044
1045 case $format in
1046
1047 completion)
1048 # Build "sortkey|path" rows, sort by the leading key descending, then
1049 # strip the key+'|' prefix to keep just the paths (the key is never
1050 # user-visible). The key MUST be an integer: `${(@On)}' numeric sort
1051 # compares each run of digits on its own, so a raw float rank orders by
1052 # its fractional digit-run rather than its value -- "100.5" would sort
1053 # below "100.25" (5 < 25). Scale by 100 and drop the decimal so two
1054 # digits of resolution survive (what the old `%.2f' rows preserved) as a
1055 # single integer digit-run. (Negative `-t' ranks still sort by
1056 # magnitude, since `n' ignores the sign -- unchanged from the `%.2f'
1057 # rows, i.e. a pre-existing quirk, not introduced here.)
1058 local sortkey
1059 for ((i=1; i<=${#kv}; i+=2)); do
1060 sortkey=$(( kv[i+1] * 100 ))
1061 descending_list+=( "${sortkey%.*}|${kv[i]}" )
1062 done
1063 descending_list=( ${${(@On)descending_list}#*\|} )
1064 print -rl -- $descending_list
1065 ;;
1066
1067 list)
1068 # The bare `z -l' fast path (no query) inlines an equivalent
1069 # formatting block straight on $lines to skip this pipeline --
1070 # keep the two list formatters in sync.
1071 local path_to_display
1072 local -a displayed_paths
1073 for ((i=1; i<=${#kv}; i+=2)); do
1074 x=${kv[i]} v=${kv[i+1]}
1075 (( v )) || continue
1076 displayed_paths+=( $x )
1077 path_to_display=$x
1078 (( ZSHZ_TILDE )) &&
1079 path_to_display=${path_to_display/#${HOME}/\~}
1080 # Right-pad the integer rank to 10 chars, as `printf "%-10d %s\n"'
1081 # used to, but in parameter expansion. The padding must be
1082 # conditional: `%-10d' never shortened anything, but a bare
1083 # `${(r:10:)}' *truncates* a rank longer than 10 characters -- an
1084 # 11-character `-t' rank (sign + 10 digits, e.g. from a zeroed or
1085 # hand-imported time field more than ~31.7 years old) or a frecency
1086 # rank inflated by a raised $ZSHZ_MAX_SCORE would lose its last
1087 # digits, garbling both the displayed figure and the numeric sort
1088 # below. The `%.*' strip drops frecency's decimal tail
1089 # ("30000.0" -> "30000") to match what `%-10d' produced.
1090 v=${v%.*}
1091 (( ${#v} < 10 )) && v=${(r:10:)v}
1092 output+=( "$v $path_to_display" )
1093 done
1094 # Recompute the common root over the entries that survived the rank
1095 # filter above: $common, computed at the top of this function,
1096 # covers *every* match -- including rank-0 entries hidden from the
1097 # listing -- so it could name a root the visible entries do not
1098 # share. The bare `z -l' fast path filters rank-0 entries before
1099 # looking for a root, and the two formatters must produce identical
1100 # output. (The jump arm below still uses the full-match root: what
1101 # `z foo' jumps to is a separate question from what a listing
1102 # displays.)
1103 common=''
1104 if (( $#displayed_paths )); then
1105 _zshz_find_common_root $displayed_paths
1106 common=$REPLY
1107 # A listing must never leave a jump target in REPLY.
1108 REPLY=''
1109 fi
1110 if [[ -n $common ]]; then
1111 (( ZSHZ_TILDE )) && common=${common/#${HOME}/\~}
1112 (( $#output > 1 )) && printf "%-10s %s\n" 'common:' $common
1113 fi
1114 if (( $#output )); then
1115 # -lt: most-recent first (descending); -lr and default -l:
1116 # ascending rank.
1117 if (( $+opts[-t] )); then
1118 print -rl -- ${(@On)output}
1119 else
1120 print -rl -- ${(@on)output}
1121 fi
1122 fi
1123 ;;
1124
1125 *)
1126 if (( ! ZSHZ_UNCOMMON )) && [[ -n $common ]]; then
1127 REPLY=$common
1128 else
1129 REPLY=${(P)match}
1130 fi
1131 ;;
1132 esac
1133 }
1134
1135 ############################################################
1136 # Match a pattern by rank, time, or a combination of the
1137 # two, and output the results as completions, a list, or a
1138 # best match.
1139 #
1140 # Globals:
1141 # ZSHZ
1142 # ZSHZ_CASE
1143 # ZSHZ_KEEP_DIRS
1144 # ZSHZ_TRAILING_SLASH
1145 #
1146 # Arguments:
1147 # $1 Pattern to match
1148 # $2 Matching method (rank, time, or [default] frecency)
1149 # $3 Output format (completion, list, or [default] store
1150 # in REPLY)
1151 ############################################################
1152 _zshz_find_matches() {
1153 setopt LOCAL_OPTIONS NO_EXTENDED_GLOB
1154
1155 local fnd=$1 method=$2 format=$3
1156
1157 local line dir path_field rank_field time_field rank dx
1158 local -A matches imatches
1159 local best_match ibest_match hi_rank=-9999999999 ihi_rank=-9999999999
1160 local -i keep
1161
1162 # Hoist loop-invariants. $fnd, $1, and $ZSHZ_TRAILING_SLASH don't
1163 # change inside the per-line loop, so the space-to-glob
1164 # substitution, the `${1:l} == $1' check, and the `:l' on $q were
1165 # pure waste when recomputed N times. The `q_lower' precompute
1166 # lets `${~q_lower}' replace `${~q:l}' in the case-insensitive
1167 # branches: same expanded pattern, compiled once.
1168 local q=${fnd//[[:space:]]/\*}
1169 local q_lower=${q:l}
1170 local -i is_lowercase_query=0
1171 [[ ${1:l} == $1 ]] && is_lowercase_query=1
1172 local -i trail=${ZSHZ_TRAILING_SLASH:-0}
1173 local now=$EPOCHSECONDS
1174
1175 # This flag is consumed by the ZSHZ_UNCOMMON trimming block, which must know
1176 # whether the match it is about to trim was found case-insensitively. Clear
1177 # it at the start of every search so a value left over from a previous call
1178 # -- e.g. a tab-completion, which sets it but never runs the trimming block
1179 # that would reset it -- can't steer this search into the wrong branch. The
1180 # authoritative value is set below, from whichever match actually wins.
1181 ZSHZ[CASE_INSENSITIVE]=0
1182
1183 for line in $lines; do
1184 path_field=${line%%\|*}
1185
1186 # Filter non-existent paths (honoring ZSHZ_KEEP_DIRS) inline so we
1187 # walk $lines once instead of twice. The `keep=1; break' inside the
1188 # inner loop also fixes a latent bug: the previous existence-check
1189 # loop had no `break' after appending, so a non-existent path that
1190 # matched multiple ZSHZ_KEEP_DIRS patterns was processed more than
1191 # once.
1192 if [[ ! -d $path_field ]]; then
1193 keep=0
1194 for dir in ${(@)ZSHZ_KEEP_DIRS}; do
1195 if [[ $path_field == ${dir}/* || $path_field == $dir || $dir == '/' ]]; then
1196 keep=1
1197 break
1198 fi
1199 done
1200 (( keep )) || continue
1201 fi
1202
1203 rank_field=${${line%\|*}#*\|}
1204 time_field=${line##*\|}
1205
1206 case $method in
1207 rank) rank=$rank_field ;;
1208 time) (( rank = time_field - now )) ;;
1209 *)
1210 # Frecency routine: weight a path's stored frequency (rank_field)
1211 # by how recently it was visited (dx seconds ago). 10000 scales
1212 # the result into integer-comparable territory; the 3.75 / (...)
1213 # term decays from 3 (just now) toward 0 as dx grows, so older
1214 # paths lose rank. This is the canonical copy; the bare `z -l'
1215 # fast path inlines the same formula -- keep the two in sync.
1216 (( dx = now - time_field ))
1217 rank=$(( 10000 * rank_field * (3.75/( (0.0001 * dx + 1) + 0.25)) ))
1218 ;;
1219 esac
1220
1221 local path_field_normalized=$path_field
1222 (( trail )) && path_field_normalized=${path_field%/}/
1223
1224 # If $ZSHZ_CASE is 'ignore', be case-insensitive.
1225 #
1226 # If it's 'smart', be case-insensitive unless the string to be matched
1227 # includes capital letters.
1228 #
1229 # Otherwise, the default behavior of Zsh-z is to match case-sensitively if
1230 # possible, then to fall back on a case-insensitive match if possible.
1231 #
1232 # Track best_match / ibest_match directly from $rank in each branch so
1233 # we never have to math-subscript matches[] / imatches[] -- the math
1234 # parser interprets shell-special chars in associative-array keys as
1235 # syntax (rupa/z#246), and the workaround used to be a seven-char
1236 # escape pass on every line. Comparing the $rank scalar to the running
1237 # max sidesteps the subscript entirely.
1238 if [[ $ZSHZ_CASE == 'smart' ]] && (( is_lowercase_query )) &&
1239 [[ ${path_field_normalized:l} == ${~q_lower} ]]; then
1240 imatches[$path_field]=$rank
1241 if (( rank > ihi_rank )); then
1242 ibest_match=$path_field
1243 ihi_rank=$rank
1244 fi
1245 elif [[ $ZSHZ_CASE != 'ignore' && $path_field_normalized == ${~q} ]]; then
1246 matches[$path_field]=$rank
1247 if (( rank > hi_rank )); then
1248 best_match=$path_field
1249 hi_rank=$rank
1250 fi
1251 elif [[ $ZSHZ_CASE != 'smart' && ${path_field_normalized:l} == ${~q_lower} ]]; then
1252 imatches[$path_field]=$rank
1253 if (( rank > ihi_rank )); then
1254 ibest_match=$path_field
1255 ihi_rank=$rank
1256 fi
1257 fi
1258 done
1259
1260 # Return 1 when there are no matches
1261 [[ -z $best_match && -z $ibest_match ]] && return 1
1262
1263 if [[ -n $best_match ]]; then
1264 _zshz_output matches best_match $format
1265 elif [[ -n $ibest_match ]]; then
1266 # The winning match is the case-insensitive one; tell the ZSHZ_UNCOMMON
1267 # trimmer to count case-insensitively. A case-sensitive winner (the branch
1268 # above) correctly leaves the flag at the 0 set at the top of the search.
1269 ZSHZ[CASE_INSENSITIVE]=1
1270 _zshz_output imatches ibest_match $format
1271 fi
1272 }
1273
1274 # THE MAIN ROUTINE
1275
1276 local -A opts
1277
1278 zparseopts -E -D -A opts -- \
1279 -add \
1280 -complete \
1281 c \
1282 e \
1283 h \
1284 -help \
1285 l \
1286 r \
1287 R \
1288 t \
1289 x
1290
1291 if [[ $1 == '--' ]]; then
1292 shift
1293 elif [[ -n ${(M)@:#-*} && -z $compstate ]]; then
1294 print "Improper option(s) given."
1295 _zshz_usage
1296 return 1
1297 fi
1298
1299 # -r (rank) and -t (recent) name different, mutually exclusive sort keys, so
1300 # asking for both is contradictory. Reject it rather than letting an arbitrary
1301 # one win -- the options loop below visits ${(k)opts} in hash order, so a
1302 # silent winner would not even be predictable. Skipped when --complete is set:
1303 # the completion widget always passes it, an error must not reach the terminal
1304 # mid-completion, and the sort order is merely cosmetic for a completion list.
1305 if (( ${+opts[-r]} && ${+opts[-t]} && ! ${+opts[--complete]} )); then
1306 print "${ZSHZ_CMD:-${_Z_CMD:-z}}: options -r and -t cannot be combined." >&2
1307 return 1
1308 fi
1309
1310 local opt output_format method='frecency' fnd prefix req
1311
1312 for opt in ${(k)opts}; do
1313 case $opt in
1314 --add)
1315 # Don't change the database when invoked via --complete (e.g., from
1316 # tab completion).
1317 (( ${+opts[--complete]} )) && continue
1318 [[ ! -d $* ]] && return 1
1319 local dir
1320 # Cygwin and MSYS2 have a hard time with relative paths expressed from /
1321 if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then
1322 set -- "/$*"
1323 fi
1324 if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
1325 dir=${*:a}
1326 else
1327 dir=${*:A}
1328 fi
1329 _zshz_add_or_remove_path --add "$dir"
1330 return
1331 ;;
1332 --complete)
1333 if [[ -s $datafile && ${ZSHZ_COMPLETION:-frecent} == 'legacy' ]]; then
1334 lines=( ${(f)"$(< $datafile)"} )
1335 # Discard entries that are incomplete or incorrectly formatted
1336 lines=( ${(M)lines:#/*\|[[:digit:]]##[.,]#[[:digit:]]#\|[[:digit:]]##} )
1337 _zshz_legacy_complete "$1"
1338 return
1339 fi
1340 output_format='completion'
1341 ;;
1342 -c) [[ $* == ${PWD}/* || $PWD == '/' ]] || prefix="$PWD " ;;
1343 -h|--help)
1344 (( ${+opts[--complete]} )) && continue
1345 _zshz_usage
1346 return
1347 ;;
1348 # --complete (completion mode) always wins over -l, independent of the
1349 # order ${(k)opts} happens to visit them: completing `z -l ...' must still
1350 # emit bare paths for compadd, never the rank-padded rows of a list.
1351 -l) (( ${+opts[--complete]} )) || output_format='list' ;;
1352 -r) method='rank' ;;
1353 -t) method='time' ;;
1354 -x)
1355 (( ${+opts[--complete]} )) && continue
1356 # Cygwin and MSYS2 have a hard time with relative paths expressed from /
1357 if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then
1358 set -- "/$*"
1359 fi
1360 _zshz_add_or_remove_path --remove $*
1361 return
1362 ;;
1363 esac
1364 done
1365
1366 # Load the datafile into an array and parse it
1367 lines=( ${(f)"$(< $datafile)"} )
1368 # Discard entries that are incomplete or incorrectly formatted
1369 lines=( ${(M)lines:#/*\|[[:digit:]]##[.,]#[[:digit:]]#\|[[:digit:]]##} )
1370
1371 req="$*"
1372 fnd="$prefix$*"
1373
1374 [[ -n $fnd && $fnd != "$PWD " ]] || {
1375 [[ $output_format != 'completion' ]] && output_format='list'
1376 }
1377
1378 #########################################################
1379 # Allow the user to specify directory-changing command
1380 # using $ZSHZ_CD (default: builtin cd).
1381 #
1382 # Globals:
1383 # ZSHZ_CD
1384 #
1385 # Arguments:
1386 # $* Path
1387 #########################################################
1388 zshz_cd() {
1389 setopt LOCAL_OPTIONS NO_WARN_CREATE_GLOBAL
1390
1391 if [[ -z $ZSHZ_CD ]]; then
1392 builtin cd "$*"
1393 else
1394 ${=ZSHZ_CD} "$*"
1395 fi
1396 }
1397
1398 #########################################################
1399 # If $ZSHZ_ECHO == 1, display paths as you jump to them.
1400 # If it is also the case that $ZSHZ_TILDE == 1, display
1401 # the home directory as a tilde.
1402 #
1403 # Globals:
1404 # ZSHZ_ECHO
1405 # ZSHZ_TILDE
1406 #########################################################
1407 _zshz_echo() {
1408 if (( ZSHZ_ECHO )); then
1409 if (( ZSHZ_TILDE )); then
1410 print -r -- ${PWD/#${HOME}/\~}
1411 else
1412 print -r -- $PWD
1413 fi
1414 fi
1415 }
1416
1417 if [[ ${@: -1} == /* ]] && (( ! $+opts[-e] && ! $+opts[-l] )); then
1418 # cd if possible; echo the new path if $ZSHZ_ECHO == 1
1419 [[ -d ${@: -1} ]] && zshz_cd ${@: -1} && _zshz_echo && return
1420 fi
1421
1422 # Fast path: bare `zshz -l' (no query, list format). Skip the
1423 # `_zshz_find_matches' / `_zshz_output' pipeline -- there is nothing
1424 # to match against, no `matches[]'/`imatches[]' to maintain, no
1425 # case-mode branching, no `${(Pkv)...}' copy. Build the formatted
1426 # output array directly, then sort and print. Mirrors the list arm
1427 # of `_zshz_output' but operates straight on $lines.
1428 if [[ $output_format == 'list' && -z $fnd ]]; then
1429 local line path_field rank_field time_field rank dx path_to_display dir
1430 local common now=$EPOCHSECONDS
1431 local -a output paths
1432 local -i keep
1433
1434 for line in $lines; do
1435 path_field=${line%%\|*}
1436
1437 if [[ ! -d $path_field ]]; then
1438 keep=0
1439 for dir in ${(@)ZSHZ_KEEP_DIRS}; do
1440 if [[ $path_field == ${dir}/* || $path_field == $dir || $dir == '/' ]]; then
1441 keep=1
1442 break
1443 fi
1444 done
1445 (( keep )) || continue
1446 fi
1447
1448 rank_field=${${line%\|*}#*\|}
1449 time_field=${line##*\|}
1450 case $method in
1451 rank) rank=$rank_field ;;
1452 time) (( rank = time_field - now )) ;;
1453 *)
1454 # Frecency routine -- see _zshz_find_matches for the canonical
1455 # copy and the constants' rationale; keep the two in sync.
1456 (( dx = now - time_field ))
1457 rank=$(( 10000 * rank_field * (3.75/( (0.0001 * dx + 1) + 0.25)) ))
1458 ;;
1459 esac
1460 (( rank )) || continue
1461
1462 paths+=( $path_field )
1463 path_to_display=$path_field
1464 (( ZSHZ_TILDE )) && path_to_display=${path_to_display/#${HOME}/\~}
1465 # Conditional padding, never a bare `${(r:10:)}' -- see the list arm
1466 # of `_zshz_output' for why a rank must not be truncated.
1467 rank=${rank%.*}
1468 (( ${#rank} < 10 )) && rank=${(r:10:)rank}
1469 output+=( "$rank $path_to_display" )
1470 done
1471
1472 if (( $#paths )); then
1473 _zshz_find_common_root $paths
1474 common=$REPLY
1475 REPLY=
1476 fi
1477
1478 if [[ -n $common ]]; then
1479 (( ZSHZ_TILDE )) && common=${common/#${HOME}/\~}
1480 (( $#output > 1 )) && printf "%-10s %s\n" 'common:' $common
1481 fi
1482
1483 if (( $#output )); then
1484 if (( $+opts[-t] )); then
1485 print -rl -- ${(@On)output}
1486 else
1487 print -rl -- ${(@on)output}
1488 fi
1489 return 0
1490 fi
1491 return 1
1492 fi
1493
1494 # With option -c, make sure query string matches beginning of matches;
1495 # otherwise look for matches anywhere in paths.
1496 #
1497 # The `$PWD != /' guard mirrors the one where the prefix is set, above. At the
1498 # root every path is already under $PWD, so no "$PWD " prefix is prepended and
1499 # $fnd stays the bare query -- which, anchored, can never match a path
1500 # beginning with `/'. Without the guard, `z -c foo' from `/' matches nothing
1501 # whatever the query. Anchoring is still right in the other prefix-less case
1502 # ($* is an absolute path under $PWD): there the query is itself anchored.
1503 if (( ${+opts[-c]} )) && [[ $PWD != '/' ]]; then
1504 _zshz_find_matches "$fnd*" $method $output_format
1505 else
1506 _zshz_find_matches "*$fnd*" $method $output_format
1507 fi
1508
1509 local ret2=$?
1510
1511 local cd
1512 # Only the default (jump/echo) format communicates a destination through
1513 # REPLY; list and completion print their results directly and leave REPLY
1514 # empty. Checking the format here is a second line of defense: even if a
1515 # future edit to `_zshz_output' lets a stray REPLY escape again, a listing
1516 # must never turn into a directory change.
1517 [[ -z $output_format ]] && cd=$REPLY
1518
1519 # New experimental "uncommon" behavior
1520 #
1521 # If the best choice at this point is something like /foo/bar/foo/bar, and the
1522 # search pattern is `bar', go to /foo/bar/foo/bar; but if the search pattern
1523 # is `foo', go to /foo/bar/foo
1524 if (( ZSHZ_UNCOMMON )) && [[ -n $cd ]]; then
1525 if [[ -n $cd ]]; then
1526
1527 # In the search pattern, replace spaces with *
1528 local q=${fnd//[[:space:]]/\*}
1529 q=${q%/} # Trailing slash has to be removed
1530
1531 # As long as the best match is not case-insensitive
1532 if (( ! ZSHZ[CASE_INSENSITIVE] )); then
1533 # Count the number of characters in $cd that $q matches
1534 local q_chars=$(( ${#cd} - ${#${cd//${~q}/}} ))
1535 # Try dropping directory elements from the right; stop when it affects
1536 # how many times the search pattern appears
1537 until (( ( ${#cd:h} - ${#${${cd:h}//${~q}/}} ) != q_chars )); do
1538 # ${cd:h} of `/' is `/', so without this guard the trim could spin
1539 # forever once it reaches the root (e.g. `/' in the database with a
1540 # pattern that matches zero characters there).
1541 [[ ${cd:h} == $cd ]] && break
1542 cd=${cd:h}
1543 done
1544
1545 # If the best match is case-insensitive
1546 else
1547 local q_chars=$(( ${#cd} - ${#${${cd:l}//${~${q:l}}/}} ))
1548 until (( ( ${#cd:h} - ${#${${${cd:h}:l}//${~${q:l}}/}} ) != q_chars )); do
1549 # See the case-sensitive branch: guard against ${cd:h} no longer
1550 # changing once the trim reaches the root.
1551 [[ ${cd:h} == $cd ]] && break
1552 cd=${cd:h}
1553 done
1554 fi
1555
1556 ZSHZ[CASE_INSENSITIVE]=0
1557 fi
1558 fi
1559
1560 if (( ret2 == 0 )) && [[ -n $cd ]]; then
1561 if (( $+opts[-e] )); then # echo
1562 (( ZSHZ_TILDE )) && cd=${cd/#${HOME}/\~}
1563 print -r -- "$cd"
1564 else
1565 # cd if possible; echo the new path if $ZSHZ_ECHO == 1
1566 [[ -d $cd ]] && zshz_cd "$cd" && _zshz_echo
1567 fi
1568 else
1569 # if $req is a valid path, cd to it; echo the new path if $ZSHZ_ECHO == 1
1570 if ! (( $+opts[-e] || $+opts[-l] )) && [[ -d $req ]]; then
1571 zshz_cd "$req" && _zshz_echo
1572 else
1573 return $ret2
1574 fi
1575 fi
1576}
1577
1578alias ${ZSHZ_CMD:-${_Z_CMD:-z}}='zshz 2>&1'
1579
1580############################################################
1581# precmd - add path to datafile unless `z -x' has just been
1582# run
1583#
1584# Globals:
1585# ZSHZ
1586############################################################
1587_zshz_precmd() {
1588 # Protect against `setopt NO_UNSET'
1589 setopt LOCAL_OPTIONS UNSET
1590
1591 # Do not add PWD to datafile when in HOME directory, or
1592 # if `z -x' has just been run
1593 [[ $PWD == "$HOME" ]] || (( ZSHZ[DIRECTORY_REMOVED] )) && return
1594
1595 # Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS
1596 local exclude
1597 for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
1598 case $PWD in
1599 ${exclude}|${exclude}/*) return ;;
1600 esac
1601 done
1602
1603 # Add PWD to the datafile. Background the write so the prompt doesn't wait on
1604 # read + tempfile + rename + chown -- which is tens of ms per prompt on
1605 # 9P-bridged or VHD-backed paths. Backgrounding is safe under develop's
1606 # lock design: the `always { zsystem flock -u $lockfd }' block in
1607 # _zshz_add_or_remove_path guarantees the parent never holds an open
1608 # lockfd between precmd invocations (so a `&!' fork can't inherit one),
1609 # and ZSHZ_LOCK_TIMEOUT (default 1s) bounds contention so a stuck holder
1610 # can't pile up writers. `&!' is zsh background + disown: no wrapper
1611 # subshell, no job-table entry, no "Done" line at the next prompt.
1612 #
1613 # Do not restore the old foreground carve-out for Cygwin/MSYS2. It was
1614 # right when backgrounding meant a subshell plus a job (two forks) and
1615 # writes were line-by-line; with one disowned fork and batched writes,
1616 # measurement (June 2026, Cygwin zsh 5.8 and MSYS2 zsh 5.9) shows ~10-12ms
1617 # at the prompt for `&!' vs. ~30ms for a foreground add at 300 datafile
1618 # entries -- and ~300ms at 1,000 entries, since the foreground cost grows
1619 # with the datafile while the fork cost stays flat.
1620 #
1621 # `2> /dev/null' is what actually enforces the "stay quiet at every prompt"
1622 # rule that $_zshz_quiet_add describes. That marker can only gate Zsh-z's own
1623 # `print's; it cannot reach the external and builtin commands further down the
1624 # --add path -- `mkdir -p', `id -ng', ${ZSHZ[CHOWN]}, the deliberately loud
1625 # datafile-creation retry, or Zsh's own redirection diagnostics -- and any of
1626 # those can fail when $ZSHZ_DATA sits on an unwritable or unmounted directory,
1627 # or when $ZSHZ_OWNER names a user `id' can't resolve. Suppressing at the fork
1628 # covers every such site at once, including ones added later, whereas
1629 # suppressing site by site has to be kept in sync forever. Nothing actionable
1630 # is lost: a foreground `z --add .' still reports in full, which is exactly
1631 # the diagnostic the lock comment above tells the user to run.
1632 local _zshz_quiet_add=1
1633 zshz --add "$PWD" 2> /dev/null &!
1634
1635 # See https://github.com/rupa/z/pull/247/commits/081406117ea42ccb8d159f7630cfc7658db054b6
1636 : $RANDOM
1637}
1638
1639############################################################
1640# chpwd
1641#
1642# When the $PWD is removed from the datafile with `z -x',
1643# Zsh-z refrains from adding it again until the user has
1644# left the directory.
1645#
1646# Globals:
1647# ZSHZ
1648############################################################
1649_zshz_chpwd() {
1650 ZSHZ[DIRECTORY_REMOVED]=0
1651}
1652
1653autoload -Uz add-zsh-hook
1654
1655add-zsh-hook precmd _zshz_precmd
1656add-zsh-hook chpwd _zshz_chpwd
1657
1658############################################################
1659# Completion
1660############################################################
1661
1662# Standardized $0 handling
1663# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html
16640="${${ZERO:-${0:#${ZSH_ARGZERO-}}}:-${(%):-%N}}"
16650="${${(M)0:#/*}:-$PWD/$0}"
1666
1667# Capture the plugin directory while $0 still names this file: inside the
1668# unload function, $0 is the function name (FUNCTION_ARGZERO), which `:A'
1669# would resolve against $PWD.
1670ZSHZ[PLUGIN_DIR]=${0:A:h}
1671
1672# Add the plugin directory to $fpath only when nothing else has already put it
1673# there, and record having done so, so that unload can take back this entry
1674# and leave a plugin manager's alone.
1675#
1676# The record is only ever set, never cleared: on a re-source the directory is
1677# already present -- because this file added it the first time -- and clearing
1678# the record then would strand the entry in $fpath at unload. `typeset -gA'
1679# above preserves the value across that re-source.
1680if (( ${fpath[(ie)${ZSHZ[PLUGIN_DIR]}]} > ${#fpath} )); then
1681 fpath=( "${ZSHZ[PLUGIN_DIR]}" "${fpath[@]}" )
1682 # Record the path itself, not a boolean. $ZSHZ[PLUGIN_DIR] is rewritten by
1683 # every source, so a flag would end up describing whichever directory was
1684 # sourced last: re-sourcing from a second, manager-owned installation would
1685 # make unload drop *that* entry and strand the one this plugin actually
1686 # added. Newline-separated, since a path may contain spaces, and split with
1687 # `${(f)...}' at unload.
1688 ZSHZ[ADDED_FPATH]="${ZSHZ[ADDED_FPATH]:+${ZSHZ[ADDED_FPATH]}
1689}${ZSHZ[PLUGIN_DIR]}"
1690fi
1691
1692# Save the existing Tab binding so that the completion widget can invoke it,
1693# but being careful not to create a situation where the widget ends up calling
1694# itself and causing infinite recursion if this script is re-sourced.
1695if (( ! ${+widgets[_zshz_zle_completion_widget]} )); then
1696 ZSHZ[TAB_BINDING]="${$(bindkey -M main '^I')##* }"
1697fi
1698
1699############################################################
1700# ZLE widget to fix spaces-as-wildcards completion
1701#
1702# When completing a Zsh-z command with multiple search terms
1703# (e.g. `z us lo bi'), collapse the terms into a single
1704# wildcard-joined word (e.g. `z us*lo*bi') before triggering
1705# completion. This causes compadd to replace the whole query
1706# with the matched path rather than just the last word.
1707#
1708# Globals:
1709# ZSHZ_CMD
1710############################################################
1711_zshz_zle_completion_widget() {
1712
1713 setopt LOCAL_OPTIONS EXTENDED_GLOB NO_KSH_ARRAYS NO_SH_WORD_SPLIT
1714
1715 local cmd=${ZSHZ_CMD:-${_Z_CMD:-z}}
1716
1717 # Ensure tab completion works under `setopt COMPLETE_ALIASES'. Under that
1718 # option zsh looks up `_comps[$cmd]' verbatim rather than expanding the
1719 # alias to `zshz' first; compinit's static `#compdef' tag in `_zshz' is
1720 # parsed literally (no parameter expansion) and only covers the literal
1721 # `zshz' command. Run once -- the guard short-circuits on subsequent Tabs.
1722 # Record what was registered, so `zsh-z_plugin_unload' can take back exactly
1723 # this entry and nothing else. Keyed on the effect rather than compdef's exit
1724 # status: if the mapping did not land, there is nothing to take back.
1725 if (( ! ${+_comps[$cmd]} )); then
1726 compdef _zshz $cmd 2> /dev/null
1727 # Append rather than overwrite. Re-sourcing with a changed $ZSHZ_CMD
1728 # registers a second command while the first mapping is still live, and a
1729 # single slot would forget the earlier one and strand it at unload. Space-
1730 # separated, like $ZSHZ[FUNCTIONS], and split with `${=...}' there.
1731 [[ ${_comps[$cmd]-} == '_zshz' ]] &&
1732 ZSHZ[COMPDEF]="${ZSHZ[COMPDEF]:+${ZSHZ[COMPDEF]} }$cmd"
1733 fi
1734
1735 # If a trailing space was added after an already-completed absolute path
1736 # (e.g. `z /usr/local/bin '), a second Tab would otherwise re-trigger
1737 # completion on an empty word and insert a duplicate. Bail out early.
1738 if [[ $LBUFFER[-1] == ' ' && ${${LBUFFER% }##* } == [/~]* ]]; then
1739 return
1740 fi
1741
1742 # Only act when there are at least two words after the command
1743 if [[ $LBUFFER == ${cmd}\ *\ * ]]; then
1744 local after=${LBUFFER#${cmd} }
1745 local -a parts option_parts search_parts
1746 local p past_options=0
1747
1748 parts=( ${(z)after} )
1749 for p in $parts; do
1750 if (( ! past_options )) && [[ $p == (--|-[cehlrRtx]##|--add|--complete|--help) ]]; then
1751 option_parts+=( $p )
1752 # `--' terminates option parsing; subsequent tokens are positional,
1753 # even if they happen to look like options.
1754 [[ $p == -- ]] && past_options=1
1755 else
1756 past_options=1
1757 search_parts+=( $p )
1758 fi
1759 done
1760
1761 if (( ${#search_parts} > 1 )); then
1762 LBUFFER="${cmd}${option_parts:+ ${(j: :)option_parts}} ${(j:*:)search_parts}"
1763 fi
1764 fi
1765
1766 # If Tab had a non-default binding, continue to use it; otherwise the default
1767 # expand-or-complete gets used.
1768 zle ${ZSHZ[TAB_BINDING]:-expand-or-complete}
1769}
1770
1771# Register the widget and bind to Tab, but only if this script has not already
1772# been sourced -- avoid infinite recursion.
1773if (( ! ${+widgets[_zshz_zle_completion_widget]} )); then
1774 zle -N _zshz_zle_completion_widget
1775 bindkey -M main '^I' _zshz_zle_completion_widget
1776fi
1777
1778############################################################
1779# zsh-z functions
1780############################################################
1781ZSHZ[FUNCTIONS]='_zshz_usage
1782 _zshz_realpath
1783 _zshz_add_or_remove_path
1784 _zshz_update_datafile
1785 _zshz_legacy_complete
1786 _zshz_find_common_root
1787 _zshz_output
1788 _zshz_find_matches
1789 zshz_cd
1790 _zshz_echo
1791 zshz
1792 _zshz_precmd
1793 _zshz_chpwd
1794 _zshz
1795 _zshz_zle_completion_widget'
1796
1797############################################################
1798# Enable WARN_NESTED_VAR for functions listed in
1799# ZSHZ[FUNCTIONS]
1800############################################################
1801(( ${+ZSHZ_DEBUG} )) && () {
1802 if is-at-least 5.4.0; then
1803 local x
1804 for x in ${=ZSHZ[FUNCTIONS]}; do
1805 functions -W $x
1806 done
1807 fi
1808}
1809
1810############################################################
1811# Unload function
1812#
1813# See https://github.com/agkozak/Zsh-100-Commits-Club/blob/master/Zsh-Plugin-Standard.adoc#unload-fun
1814#
1815# Globals:
1816# ZSHZ
1817# ZSHZ_CMD
1818############################################################
1819zsh-z_plugin_unload() {
1820 emulate -L zsh
1821
1822 add-zsh-hook -D precmd _zshz_precmd
1823 add-zsh-hook -d chpwd _zshz_chpwd
1824
1825 zle -D _zshz_zle_completion_widget
1826
1827 # Only restore Tab binding if it is still bound to our widget; otherwise
1828 # leave it alone.
1829 local _zshz_current_tab
1830 _zshz_current_tab="$(bindkey -M main '^I' 2>/dev/null || true)"
1831 if [[ ${_zshz_current_tab##* } == _zshz_zle_completion_widget ]]; then
1832 bindkey -M main '^I' "${ZSHZ[TAB_BINDING]:-expand-or-complete}"
1833 fi
1834
1835 local x
1836 for x in ${=ZSHZ[FUNCTIONS]}; do
1837 (( ${+functions[$x]} )) && unfunction $x
1838 done
1839
1840 # The directory captured at source time -- $0 here is the function name,
1841 # not the plugin file. Read it before ZSHZ is unset.
1842 #
1843 # Only when this plugin was the one that added it. A plugin manager that put
1844 # the directory on $fpath owns that entry: taking it away would break
1845 # autoloads for anything else living there and leave the manager believing
1846 # its configuration is intact. And drop a single occurrence rather than
1847 # filtering every match -- at most one of any duplicates can be ours.
1848 #
1849 # `(ie)', not `(i)': without the `e' the subscript treats the stored path as
1850 # a *pattern*, so a plugin directory containing `[', `*' or `?' would not
1851 # match itself and the entry would be left behind. The source-time lookup
1852 # already uses `(ie)'; these two must agree.
1853 local _zshz_dir
1854 integer _zshz_fp
1855 for _zshz_dir in ${(f)ZSHZ[ADDED_FPATH]-}; do
1856 [[ -n $_zshz_dir ]] || continue
1857 _zshz_fp=${fpath[(ie)$_zshz_dir]}
1858 (( _zshz_fp <= ${#fpath} )) && fpath[$_zshz_fp]=()
1859 done
1860
1861 # Take back the completion mapping the widget installed on its first Tab.
1862 # Without this the entry outlives the function it names -- `_zshz' is
1863 # unfunctioned above and the plugin directory has just left $fpath, so a
1864 # later completion on that command looks up something unloadable.
1865 #
1866 # Only this one entry. compinit's own registrations (`_comps[zshz]', from the
1867 # static `#compdef' tag) are deliberately left in place: nothing re-runs
1868 # compinit when the plugin is sourced again, so removing them would break
1869 # completion for the literal `zshz' command until the user re-ran it by hand.
1870 # This entry has no such problem -- the widget re-registers it on the next
1871 # Tab after a reload.
1872 #
1873 # `$ZSHZ[COMPDEF]' is the ownership record: the registration above never
1874 # overwrites an existing mapping, so one Zsh-z did not create must survive
1875 # unload. Re-check the value too, in case it was repointed since.
1876 local _zshz_compdef
1877 for _zshz_compdef in ${=ZSHZ[COMPDEF]-}; do
1878 [[ ${_comps[$_zshz_compdef]-} == '_zshz' ]] &&
1879 compdef -d "$_zshz_compdef" 2> /dev/null
1880 done
1881
1882 unset ZSHZ
1883
1884 (( ${+aliases[${ZSHZ_CMD:-${_Z_CMD:-z}}]} )) &&
1885 unalias ${ZSHZ_CMD:-${_Z_CMD:-z}}
1886
1887 unfunction $0
1888}
1889
1890# vim: fdm=indent:ts=2:et:sts=2:sw=2: