1 ################################################################################
2 # Zsh-z - jump around with Zsh - A native Zsh version of z without awk, sort,
5 # https://github.com/agkozak/zsh-z
7 # Copyright (c) 2018-2022 Alexandros Kozak
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:
16 # The above copyright notice and this permission notice shall be included in all
17 # copies or substantial portions of the Software.
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
27 # z (https://github.com/rupa/z) is copyright (c) 2009 rupa deadwyler and
28 # licensed under the WTFPL license, Version 2.
30 # Zsh-z maintains a jump-list of the directories you actually use.
33 # * put something like this in your .zshrc:
34 # source /path/to/zsh-z.plugin.zsh
35 # * cd around for a while to build up the database
38 # * z foo cd to the most frecent directory matching foo
39 # * z foo bar cd to the most frecent directory matching both foo and bar
40 # (e.g. /foo/bat/bar/quux)
41 # * z -r foo cd to the highest ranked directory matching foo
42 # * z -t foo cd to most recently accessed directory matching foo
43 # * z -l foo List matches instead of changing directories
44 # * z -e foo Echo the best match without changing directories
45 # * z -c foo Restrict matches to subdirectories of PWD
46 # * z -x Remove a directory (default: PWD) from the database
47 # * z -xR Remove a directory (default: PWD) and its subdirectories from
50 # ENVIRONMENT VARIABLES:
52 # ZSHZ_CASE -> if `ignore', pattern matching is case-insensitive; if `smart',
53 # pattern matching is case-insensitive only when the pattern is all
55 # ZSHZ_CMD -> name of command (default: z)
56 # ZSHZ_COMPLETION -> completion method (default: 'frecent'; 'legacy' for
58 # ZSHZ_DATA -> name of datafile (default: ~/.z)
59 # ZSHZ_EXCLUDE_DIRS -> array of directories to exclude from your database
61 # ZSHZ_KEEP_DIRS -> array of directories that should not be removed from the
62 # database, even if they are not currently available (default: empty)
63 # ZSHZ_MAX_SCORE -> maximum combined score the database entries can have
64 # before beginning to age (default: 9000)
65 # ZSHZ_NO_RESOLVE_SYMLINKS -> '1' prevents symlink resolution
66 # ZSHZ_OWNER -> your username (if you want use Zsh-z while using sudo -s)
67 # ZSHZ_UNCOMMON -> if 1, do not jump to "common directories," but rather drop
68 # subdirectories based on what the search string was (default: 0)
69 ################################################################################
71 autoload -U is-at-least
73 if ! is-at-least 4.3.11; then
74 print "Zsh-z requires Zsh v4.3.11 or higher." >&2 && exit
77 ############################################################
82 ############################################################
84 print "Usage: ${ZSHZ_CMD:-${_Z_CMD:-z}} [OPTION]... [ARGUMENT]
85 Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial string ARGUMENT.
87 With no ARGUMENT, list the directory history in ascending rank.
89 --add Add a directory to the database
90 -c Only match subdirectories of the current directory
91 -e Echo the best match without going to it
92 -h Display this help and exit
93 -l List all matches without going to them
95 -t Match by recent access
96 -x Remove a directory from the database (by default, the current directory)
97 -xR Remove a directory and its subdirectories from the database (by default, the current directory)" |
98 fold -s -w $COLUMNS >&2
101 # Load zsh/datetime module, if necessary
102 (( $+EPOCHSECONDS )) || zmodload zsh/datetime
104 # Load zsh/files, if necessary
105 [[ ${builtins[zf_chown]} == 'defined' &&
106 ${builtins[zf_mv]} == 'defined' &&
107 ${builtins[zf_rm]} == 'defined' ]] ||
108 zmodload -F zsh/files b:zf_chown b:zf_mv b:zf_rm
110 # Load zsh/system, if necessary
111 [[ ${modules[zsh/system]} == 'loaded' ]] || zmodload zsh/system &> /dev/null
113 # Global associative array for internal use
116 # Make sure ZSHZ_EXCLUDE_DIRS has been declared so that other scripts can
117 # simply append to it
118 (( ${+ZSHZ_EXCLUDE_DIRS} )) || typeset -gUa ZSHZ_EXCLUDE_DIRS
120 # Determine if zsystem flock is available
121 zsystem supports flock &> /dev/null && ZSHZ[USE_FLOCK]=1
123 # Determine if `print -v' is supported
124 is-at-least 5.3.0 && ZSHZ[PRINTV]=1
126 ############################################################
141 # $* Command options and arguments
142 ############################################################
145 # Don't use `emulate -L zsh' - it breaks PUSHD_IGNORE_DUPS
146 setopt LOCAL_OPTIONS NO_KSH_ARRAYS NO_SH_WORD_SPLIT EXTENDED_GLOB
147 (( ZSHZ_DEBUG )) && setopt LOCAL_OPTIONS WARN_CREATE_GLOBAL
152 # Allow the user to specify the datafile name in $ZSHZ_DATA (default: ~/.z)
153 # If the datafile is a symlink, it gets dereferenced
154 local datafile=${${ZSHZ_DATA:-${_Z_DATA:-${HOME}/.z}}:A}
156 # If the datafile is a directory, print a warning and exit
157 if [[ -d $datafile ]]; then
158 print "ERROR: Zsh-z's datafile (${datafile}) is a directory." >&2
162 # Make sure that the datafile exists before attempting to read it or lock it
164 [[ -f $datafile ]] || touch "$datafile"
166 # Bail if we don't own the datafile and $ZSHZ_OWNER is not set
167 [[ -z ${ZSHZ_OWNER:-${_Z_OWNER}} && -f $datafile && ! -O $datafile ]] &&
170 # Load the datafile into an array and parse it
171 lines=( ${(f)"$(< $datafile)"} )
172 # Discard entries that are incomplete or incorrectly formatted
173 lines=( ${(M)lines:#/*\|[[:digit:]]##[.,]#[[:digit:]]#\|[[:digit:]]##} )
175 ############################################################
176 # Add a path to or remove one from the datafile
184 # $1 Which action to perform (--add/--remove)
186 ############################################################
187 _zshz_add_or_remove_path() {
191 if [[ $action == '--add' ]]; then
193 # TODO: The following tasks are now handled by _agkozak_precmd. Dead code?
196 [[ $* == $HOME ]] && return
198 # Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS
200 for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
202 ${exclude}|${exclude}/*) return ;;
207 # A temporary file that gets copied over the datafile if all goes well
208 local tempfile="${datafile}.${RANDOM}"
210 # See https://github.com/rupa/z/pull/199/commits/ed6eeed9b70d27c1582e3dd050e72ebfe246341c
211 if (( ZSHZ[USE_FLOCK] )); then
215 # Grab exclusive lock (released when function exits)
216 zsystem flock -f lockfd "$datafile" 2> /dev/null || return
223 exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
224 _zshz_update_datafile $tmpfd "$*"
228 local xdir # Directory to be removed
230 if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
231 [[ -d ${${*:-${PWD}}:a} ]] && xdir=${${*:-${PWD}}:a}
233 [[ -d ${${*:-${PWD}}:A} ]] && xdir=${${*:-${PWD}}:a}
236 local -a lines_to_keep
237 if (( ${+opts[-R]} )); then
238 # Prompt user before deleting entire database
239 if [[ $xdir == '/' ]] && ! read -q "?Delete entire Zsh-z database? "; then
242 # All of the lines that don't match the directory to be deleted
243 lines_to_keep=( ${lines:#${xdir}\|*} )
244 # Or its subdirectories
245 lines_to_keep=( ${lines_to_keep:#${xdir%/}/**} )
247 # All of the lines that don't match the directory to be deleted
248 lines_to_keep=( ${lines:#${xdir}\|*} )
250 if [[ $lines != "$lines_to_keep" ]]; then
251 lines=( $lines_to_keep )
253 return 1 # The $PWD isn't in the datafile
255 exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
256 print -u $tmpfd -l -- $lines
261 if (( tmpfd != 0 )); then
266 if (( ret != 0 )); then
267 # Avoid clobbering the datafile if the write to tempfile failed
273 owner=${ZSHZ_OWNER:-${_Z_OWNER}}
275 if (( ZSHZ[USE_FLOCK] )); then
276 zf_mv "$tempfile" "$datafile" 2> /dev/null || zf_rm -f "$tempfile"
278 if [[ -n $owner ]]; then
279 zf_chown ${owner}:"$(id -ng ${owner})" "$datafile"
282 if [[ -n $owner ]]; then
283 zf_chown "${owner}":"$(id -ng "${owner}")" "$tempfile"
285 zf_mv -f "$tempfile" "$datafile" 2> /dev/null || zf_rm -f "$tempfile"
288 # In order to make z -x work, we have to disable zsh-z's adding
289 # to the database until the user changes directory and the
290 # chpwd_functions are run
291 if [[ $action == '--remove' ]]; then
292 ZSHZ[DIRECTORY_REMOVED]=1
296 ############################################################
297 # Read the curent datafile contents, update them, "age" them
298 # when the total rank gets high enough, and print the new
299 # contents to STDOUT.
306 # $1 File descriptor linked to tempfile
307 # $2 Path to be added to datafile
308 ############################################################
309 _zshz_update_datafile() {
314 # Characters special to the shell (such as '[]') are quoted with backslashes
315 # See https://github.com/rupa/z/issues/246
316 local add_path=${(q)2}
318 local -a existing_paths
319 local now=$EPOCHSECONDS line dir
320 local path_field rank_field time_field count x
325 # Remove paths from database if they no longer exist
326 for line in $lines; do
327 if [[ ! -d ${line%%\|*} ]]; then
328 for dir in ${(@)ZSHZ_KEEP_DIRS}; do
329 if [[ ${line%%\|*} == ${dir}/* ||
330 ${line%%\|*} == $dir ||
332 existing_paths+=( $line )
336 existing_paths+=( $line )
339 lines=( $existing_paths )
341 for line in $lines; do
342 path_field=${(q)line%%\|*}
343 rank_field=${${line%\|*}#*\|}
344 time_field=${line##*\|}
346 # When a rank drops below 1, drop the path from the database
347 (( rank_field < 1 )) && continue
349 if [[ $path_field == $add_path ]]; then
350 rank[$path_field]=$rank_field
351 (( rank[$path_field]++ ))
352 time[$path_field]=$now
354 rank[$path_field]=$rank_field
355 time[$path_field]=$time_field
357 (( count += rank_field ))
359 if (( count > ${ZSHZ_MAX_SCORE:-${_Z_MAX_SCORE:-9000}} )); then
361 for x in ${(k)rank}; do
362 print -u $fd -- "$x|$(( 0.99 * rank[$x] ))|${time[$x]}" || return 1
365 for x in ${(k)rank}; do
366 print -u $fd -- "$x|${rank[$x]}|${time[$x]}" || return 1
371 ############################################################
372 # The original tab completion method
374 # String processing is smartcase -- case-insensitive if the
375 # search string is lowercase, case-sensitive if there are
376 # any uppercase letters. Spaces in the search string are
377 # treated as *'s in globbing. Read the contents of the
378 # datafile and print matches to STDOUT.
381 # $1 The string to be completed
382 ############################################################
383 _zshz_legacy_complete() {
385 local line path_field path_field_normalized
387 # Replace spaces in the search string with asterisks for globbing
388 1=${1//[[:space:]]/*}
390 for line in $lines; do
392 path_field=${line%%\|*}
394 path_field_normalized=$path_field
395 if (( ZSHZ_TRAILING_SLASH )); then
396 path_field_normalized=${path_field%/}/
399 # If the search string is all lowercase, the search will be case-insensitive
400 if [[ $1 == "${1:l}" && ${path_field_normalized:l} == *${~1}* ]]; then
402 # Otherwise, case-sensitive
403 elif [[ $path_field_normalized == *${~1}* ]]; then
408 # TODO: Search strings with spaces in them are currently treated case-
412 ############################################################
413 # `print' or `printf' to REPLY
415 # Variable assignment through command substitution, of the
420 # requires forking a subshell; on Cygwin/MSYS2/WSL1 that can
421 # be surprisingly slow. Zsh-z avoids doing that by printing
422 # values to the variable REPLY. Since Zsh v5.3.0 that has
423 # been possible with `print -v'; for earlier versions of the
424 # shell, the values are placed on the editing buffer stack
425 # and then `read' into REPLY.
431 # Options and parameters for `print'
432 ############################################################
434 # NOTE: For a long time, ZSH's `print -v' had a tendency
435 # to mangle multibyte strings:
437 # https://www.zsh.org/mla/workers/2020/msg00307.html
439 # The bug was fixed in late 2020:
441 # https://github.com/zsh-users/zsh/commit/b6ba74cd4eaec2b6cb515748cf1b74a19133d4a4#diff-32bbef18e126b837c87b06f11bfc61fafdaa0ed99fcb009ec53f4767e246b129
443 # In order to support shells with the bug, we must use a form of `printf`,
444 # which does not exhibit the undesired behavior. See
446 # https://www.zsh.org/mla/workers/2020/msg00308.html
448 if (( ZSHZ[PRINTV] )); then
449 builtin print -v REPLY -f %s $@
452 builtin read -rz REPLY
456 ############################################################
457 # If matches share a common root, find it, and put it in
458 # REPLY for _zshz_output to use.
461 # $1 Name of associative array of matches and ranks
462 ############################################################
463 _zshz_find_common_root() {
464 local -a common_matches
467 common_matches=( ${(@Pk)1} )
469 for x in ${(@)common_matches}; do
470 if [[ -z $short ]] || (( $#x < $#short )) || [[ $x != ${short}/* ]]; then
475 [[ $short == '/' ]] && return
477 for x in ${(@)common_matches}; do
478 [[ $x != $short* ]] && return
481 _zshz_printv -- $short
484 ############################################################
485 # Calculate a common root, if there is one. Then do one of
488 # 1) Print a list of completions in frecent order;
489 # 2) List them (z -l) to STDOUT; or
490 # 3) Put a common root or best match into REPLY
496 # $1 Name of an associative array of matches and ranks
497 # $2 The best match or best case-insensitive match
498 # $3 Whether to produce a completion, a list, or a root or
500 ############################################################
503 local match_array=$1 match=$2 format=$3
505 local -a descending_list output
506 local -A output_matches
508 output_matches=( ${(Pkv)match_array} )
510 _zshz_find_common_root $match_array
516 for k in ${(@k)output_matches}; do
517 _zshz_printv -f "%.2f|%s" ${output_matches[$k]} $k
518 descending_list+=( ${(f)REPLY} )
521 descending_list=( ${${(@On)descending_list}#*\|} )
522 print -l $descending_list
526 local path_to_display
527 for x in ${(k)output_matches}; do
528 if (( ${output_matches[$x]} )); then
531 path_to_display=${path_to_display/#${HOME}/\~}
532 _zshz_printv -f "%-10d %s\n" ${output_matches[$x]} $path_to_display
533 output+=( ${(f)REPLY} )
537 if [[ -n $common ]]; then
538 (( ZSHZ_TILDE )) && common=${common/#${HOME}/\~}
539 (( $#output > 1 )) && printf "%-10s %s\n" 'common:' $common
542 if (( $+opts[-t] )); then
543 for x in ${(@On)output}; do
547 elif (( $+opts[-r] )); then
548 for x in ${(@on)output}; do
553 for x in ${(@on)output}; do
560 if (( ! ZSHZ_UNCOMMON )) && [[ -n $common ]]; then
561 _zshz_printv -- $common
563 _zshz_printv -- ${(P)match}
569 ############################################################
570 # Match a pattern by rank, time, or a combination of the
571 # two, and output the results as completions, a list, or a
581 # #1 Pattern to match
582 # $2 Matching method (rank, time, or [default] frecency)
583 # $3 Output format (completion, list, or [default] store
585 ############################################################
586 _zshz_find_matches() {
587 setopt LOCAL_OPTIONS NO_EXTENDED_GLOB
589 local fnd=$1 method=$2 format=$3
591 local -a existing_paths
592 local line dir path_field rank_field time_field rank dx escaped_path_field
593 local -A matches imatches
594 local best_match ibest_match hi_rank=-9999999999 ihi_rank=-9999999999
596 # Remove paths from database if they no longer exist
597 for line in $lines; do
598 if [[ ! -d ${line%%\|*} ]]; then
599 for dir in ${(@)ZSHZ_KEEP_DIRS}; do
600 if [[ ${line%%\|*} == ${dir}/* ||
601 ${line%%\|*} == $dir ||
603 existing_paths+=( $line )
607 existing_paths+=( $line )
610 lines=( $existing_paths )
612 for line in $lines; do
613 path_field=${line%%\|*}
614 rank_field=${${line%\|*}#*\|}
615 time_field=${line##*\|}
618 rank) rank=$rank_field ;;
619 time) (( rank = time_field - EPOCHSECONDS )) ;;
622 (( dx = EPOCHSECONDS - time_field ))
623 rank=$(( 10000 * rank_field * (3.75/((0.0001 * dx + 1) + 0.25)) ))
627 # Use spaces as wildcards
628 local q=${fnd//[[:space:]]/\*}
630 # If $ZSHZ_TRAILING_SLASH is set, use path_field with a trailing slash for matching.
631 local path_field_normalized=$path_field
632 if (( ZSHZ_TRAILING_SLASH )); then
633 path_field_normalized=${path_field%/}/
636 # If $ZSHZ_CASE is 'ignore', be case-insensitive.
638 # If it's 'smart', be case-insensitive unless the string to be matched
639 # includes capital letters.
641 # Otherwise, the default behavior of Zsh-z is to match case-sensitively if
642 # possible, then to fall back on a case-insensitive match if possible.
643 if [[ $ZSHZ_CASE == 'smart' && ${1:l} == $1 &&
644 ${path_field_normalized:l} == ${~q:l} ]]; then
645 imatches[$path_field]=$rank
646 elif [[ $ZSHZ_CASE != 'ignore' && $path_field_normalized == ${~q} ]]; then
647 matches[$path_field]=$rank
648 elif [[ $ZSHZ_CASE != 'smart' && ${path_field_normalized:l} == ${~q:l} ]]; then
649 imatches[$path_field]=$rank
652 # Escape characters that would cause "invalid subscript" errors
653 # when accessing the associative array.
654 escaped_path_field=${path_field//'\'/'\\'}
655 escaped_path_field=${escaped_path_field//'`'/'\`'}
656 escaped_path_field=${escaped_path_field//'('/'\('}
657 escaped_path_field=${escaped_path_field//')'/'\)'}
658 escaped_path_field=${escaped_path_field//'['/'\['}
659 escaped_path_field=${escaped_path_field//']'/'\]'}
661 if (( matches[$escaped_path_field] )) &&
662 (( matches[$escaped_path_field] > hi_rank )); then
663 best_match=$path_field
664 hi_rank=${matches[$escaped_path_field]}
665 elif (( imatches[$escaped_path_field] )) &&
666 (( imatches[$escaped_path_field] > ihi_rank )); then
667 ibest_match=$path_field
668 ihi_rank=${imatches[$escaped_path_field]}
669 ZSHZ[CASE_INSENSITIVE]=1
673 # Return 1 when there are no matches
674 [[ -z $best_match && -z $ibest_match ]] && return 1
676 if [[ -n $best_match ]]; then
677 _zshz_output matches best_match $format
678 elif [[ -n $ibest_match ]]; then
679 _zshz_output imatches ibest_match $format
687 zparseopts -E -D -A opts -- \
700 if [[ $1 == '--' ]]; then
702 elif [[ -n ${(M)@:#-*} && -z $compstate ]]; then
703 print "Improper option(s) given."
708 local opt output_format method='frecency' fnd prefix req
710 for opt in ${(k)opts}; do
713 [[ ! -d $* ]] && return 1
715 # Cygwin and MSYS2 have a hard time with relative paths expressed from /
716 if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then
719 if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
724 _zshz_add_or_remove_path --add "$dir"
728 if [[ -s $datafile && ${ZSHZ_COMPLETION:-frecent} == 'legacy' ]]; then
729 _zshz_legacy_complete "$1"
732 output_format='completion'
734 -c) [[ $* == ${PWD}/* || $PWD == '/' ]] || prefix="$PWD " ;;
739 -l) output_format='list' ;;
743 # Cygwin and MSYS2 have a hard time with relative paths expressed from /
744 if [[ $OSTYPE == (cygwin|msys) && $PWD == '/' && $* != /* ]]; then
747 _zshz_add_or_remove_path --remove $*
755 [[ -n $fnd && $fnd != "$PWD " ]] || {
756 [[ $output_format != 'completion' ]] && output_format='list'
759 #########################################################
760 # If $ZSHZ_ECHO == 1, display paths as you jump to them.
761 # If it is also the case that $ZSHZ_TILDE == 1, display
762 # the home directory as a tilde.
763 #########################################################
765 if (( ZSHZ_ECHO )); then
766 if (( ZSHZ_TILDE )); then
767 print ${PWD/#${HOME}/\~}
774 if [[ ${@: -1} == /* ]] && (( ! $+opts[-e] && ! $+opts[-l] )); then
775 # cd if possible; echo the new path if $ZSHZ_ECHO == 1
776 [[ -d ${@: -1} ]] && builtin cd ${@: -1} && _zshz_echo && return
779 # With option -c, make sure query string matches beginning of matches;
780 # otherwise look for matches anywhere in paths
782 # zpm-zsh/colors has a global $c, so we'll avoid math expressions here
783 if [[ ! -z ${(tP)opts[-c]} ]]; then
784 _zshz_find_matches "$fnd*" $method $output_format
786 _zshz_find_matches "*$fnd*" $method $output_format
794 # New experimental "uncommon" behavior
796 # If the best choice at this point is something like /foo/bar/foo/bar, and the # search pattern is `bar', go to /foo/bar/foo/bar; but if the search pattern
797 # is `foo', go to /foo/bar/foo
798 if (( ZSHZ_UNCOMMON )) && [[ -n $cd ]]; then
799 if [[ -n $cd ]]; then
801 # In the search pattern, replace spaces with *
802 local q=${fnd//[[:space:]]/\*}
803 q=${q%/} # Trailing slash has to be removed
805 # As long as the best match is not case-insensitive
806 if (( ! ZSHZ[CASE_INSENSITIVE] )); then
807 # Count the number of characters in $cd that $q matches
808 local q_chars=$(( ${#cd} - ${#${cd//${~q}/}} ))
809 # Try dropping directory elements from the right; stop when it affects
810 # how many times the search pattern appears
811 until (( ( ${#cd:h} - ${#${${cd:h}//${~q}/}} ) != q_chars )); do
815 # If the best match is case-insensitive
817 local q_chars=$(( ${#cd} - ${#${${cd:l}//${~${q:l}}/}} ))
818 until (( ( ${#cd:h} - ${#${${${cd:h}:l}//${~${q:l}}/}} ) != q_chars )); do
823 ZSHZ[CASE_INSENSITIVE]=0
827 if (( ret2 == 0 )) && [[ -n $cd ]]; then
828 if (( $+opts[-e] )); then # echo
829 (( ZSHZ_TILDE )) && cd=${cd/#${HOME}/\~}
832 # cd if possible; echo the new path if $ZSHZ_ECHO == 1
833 [[ -d $cd ]] && builtin cd "$cd" && _zshz_echo
836 # if $req is a valid path, cd to it; echo the new path if $ZSHZ_ECHO == 1
837 if ! (( $+opts[-e] || $+opts[-l] )) && [[ -d $req ]]; then
838 builtin cd "$req" && _zshz_echo
845 alias ${ZSHZ_CMD:-${_Z_CMD:-z}}='zshz 2>&1'
847 ############################################################
848 # precmd - add path to datafile unless `z -x' has just been
853 ############################################################
855 # Do not add PWD to datafile when in HOME directory, or
856 # if `z -x' has just been run
857 [[ $PWD == "$HOME" ]] || (( ZSHZ[DIRECTORY_REMOVED] )) && return
859 # Don't track directory trees excluded in ZSHZ_EXCLUDE_DIRS
861 for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
863 ${exclude}|${exclude}/*) return ;;
867 # It appears that forking a subshell is so slow in Windows that it is better
868 # just to add the PWD to the datafile in the foreground
869 if [[ $OSTYPE == (cygwin|msys) ]]; then
872 (zshz --add "$PWD" &)
875 # See https://github.com/rupa/z/pull/247/commits/081406117ea42ccb8d159f7630cfc7658db054b6
879 ############################################################
882 # When the $PWD is removed from the datafile with `z -x',
883 # Zsh-z refrains from adding it again until the user has
884 # left the directory.
888 ############################################################
890 ZSHZ[DIRECTORY_REMOVED]=0
893 autoload -Uz add-zsh-hook
895 add-zsh-hook precmd _zshz_precmd
896 add-zsh-hook chpwd _zshz_chpwd
898 ############################################################
900 ############################################################
902 # Standarized $0 handling
903 # (See https://github.com/agkozak/Zsh-100-Commits-Club/blob/master/Zsh-Plugin-Standard.adoc)
904 0=${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}
905 0=${${(M)0:#/*}:-$PWD/$0}
907 (( ${fpath[(ie)${0:A:h}]} <= ${#fpath} )) || fpath=( "${0:A:h}" "${fpath[@]}" )
909 ############################################################
911 ############################################################
912 ZSHZ[FUNCTIONS]='_zshz_usage
913 _zshz_add_or_remove_path
914 _zshz_update_datafile
915 _zshz_legacy_complete
917 _zshz_find_common_root
925 ############################################################
926 # Enable WARN_NESTED_VAR for functions listed in
928 ############################################################
929 (( ZSHZ_DEBUG )) && () {
930 if is-at-least 5.4.0; then
932 for x in ${=ZSHZ[FUNCTIONS]}; do
938 ############################################################
941 # See https://github.com/agkozak/Zsh-100-Commits-Club/blob/master/Zsh-Plugin-Standard.adoc#unload-fun
946 ############################################################
947 zsh-z_plugin_unload() {
950 add-zsh-hook -D precmd _zshz_precmd
951 add-zsh-hook -d chpwd _zshz_chpwd
954 for x in ${=ZSHZ[FUNCTIONS]}; do
955 (( ${+functions[$x]} )) && unfunction $x
960 fpath=( "${(@)fpath:#${0:A:h}}" )
962 (( ${+aliases[${ZSHZ_CMD:-${_Z_CMD:-z}}]} )) &&
963 unalias ${ZSHZ_CMD:-${_Z_CMD:-z}}
968 # vim: fdm=indent:ts=2:et:sts=2:sw=2: