-
-
Notifications
You must be signed in to change notification settings - Fork 80
Expand file tree
/
Copy pathzsh-z.plugin.zsh
More file actions
1890 lines (1723 loc) · 77.4 KB
/
Copy pathzsh-z.plugin.zsh
File metadata and controls
1890 lines (1723 loc) · 77.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
################################################################################
# Zsh-z - jump around with Zsh - A native Zsh version of rupa/z without awk,
# sort, date, or sed
#
# https://github.com/agkozak/zsh-z
#
# Copyright (c) 2018-2026 Alexandros Kozak
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Zsh-z maintains a jump-list of the directories you actually use.
#
# INSTALL:
# * put something like this in your .zshrc:
# source /path/to/zsh-z.plugin.zsh
# * cd around for a while to build up the database
#
# USAGE:
# * z foo cd to the most frecent directory matching foo
# * z foo bar cd to the most frecent directory matching both foo and bar
# (e.g. /foo/bat/bar/quux)
# * z -r foo cd to the highest ranked directory matching foo
# * z -t foo cd to most recently accessed directory matching foo
# * z -l foo List matches instead of changing directories
# * z -e foo Echo the best match without changing directories
# * z -c foo Restrict matches to subdirectories of PWD
# * z -x Remove a directory (default: PWD) from the database
# * z -xR Remove a directory (default: PWD) and its subdirectories from
# the database
#
# ENVIRONMENT VARIABLES:
#
# ZSHZ_CASE -> if `ignore', pattern matching is case-insensitive; if `smart',
# pattern matching is case-insensitive only when the pattern is all
# lowercase
# ZSHZ_CD -> the directory-changing command that is used (default: builtin cd)
# ZSHZ_CMD -> name of command (default: z)
# ZSHZ_COMPLETION -> completion method (default: 'frecent'; 'legacy' for
# alphabetic sorting)
# ZSHZ_DATA -> name of datafile (default: ~/.z)
# ZSHZ_DEBUG -> if set, turn on debugging aids: WARN_CREATE_GLOBAL while the
# command runs and per-function warnings (functions -W) at load time
# (default: unset)
# ZSHZ_ECHO -> if 1, print the directory name after jumping to it (default: 0)
# ZSHZ_EXCLUDE_DIRS -> array of directories to exclude from your database
# (default: empty)
# ZSHZ_KEEP_DIRS -> array of directories that should not be removed from the
# database, even if they are not currently available (default: empty)
# ZSHZ_LOCK_TIMEOUT -> seconds to wait for the lockfile before giving up
# (default: 1)
# ZSHZ_MAX_SCORE -> maximum combined score the database entries can have
# before beginning to age (default: 9000)
# ZSHZ_NO_RESOLVE_SYMLINKS -> '1' prevents symlink resolution
# ZSHZ_OWNER -> your username (if you want use Zsh-z while using sudo -s)
# ZSHZ_TILDE -> if 1, display ~ in place of the full $HOME path in output
# (default: 0)
# ZSHZ_TRAILING_SLASH -> if 1, a query ending in / matches at the end of a
# directory path (default: 0)
# ZSHZ_UNCOMMON -> if 1, do not jump to "common directories," but rather drop
# subdirectories based on what the search string was (default: 0)
################################################################################
# Minimalistic solution to allow this plugin to keep running under sh/bash/ksh
# emulation while continuing to use Zsh-only syntax features. `emulate zsh -c'
# evaluates its argument as code, so the script's own path -- `${(%):-%N}' --
# must be `${(q)}'-quoted; otherwise an install directory containing spaces or
# other shell-special characters (common on Cygwin/MSYS2 and macOS, where a
# home directory can be "C:\Users\John Smith" or "/Users/John Smith") would be
# word-split and the plugin would silently fail to re-source.
if [[ -o KSH_ARRAYS || -o SH_WORD_SPLIT ]]; then
emulate zsh -c "source ${(q)${(%):-%N}}"
return $?
fi
autoload -Uz is-at-least
if ! is-at-least 4.3.11; then
print "Zsh-z requires Zsh v4.3.11 or higher." >&2
return 1 2> /dev/null || exit 1
fi
############################################################
# The help message
#
# Globals:
# ZSHZ_CMD
############################################################
_zshz_usage() {
print "Usage: ${ZSHZ_CMD:-${_Z_CMD:-z}} [OPTION]... [ARGUMENT]
Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial string ARGUMENT.
With no ARGUMENT, list the directory history in ascending rank.
--add Add a directory to the database
-c Only match subdirectories of the current directory
-e Echo the best match without going to it
-h Display this help and exit
-l List all matches without going to them
-r Match by rank
-t Match by recent access
-x Remove a directory from the database (by default, the current directory)
-xR Remove a directory and its subdirectories from the database (by default, the current directory)" |
fold -s -w $(( COLUMNS > 0 ? COLUMNS : 80 )) >&2
}
############################################################
# Canonicalize a path in the manner of `:A' -- normalize it
# lexically as `:a' does, then resolve symlinks -- without
# requiring any of the path to exist.
#
# `${x:A}' itself cannot be trusted with a missing path on
# Zsh 4.3.11: when the top-level component of $x does not
# exist (`/gone/sub'), the realpath machinery segfaults the
# shell (upstream bug, 4.3.11 only; deeper missing
# components are handled correctly on every version). So
# apply `:A' only to the deepest ancestor of the path that
# exists -- `:A' on an existing path is safe everywhere --
# and reattach the missing components verbatim. That
# reproduces `:A' exactly: `:A' resolves the symlinks in the
# existing prefix and carries the nonexistent tail
# unchanged, and the tail cannot contain live symlinks
# precisely because it does not exist. (A broken symlink
# stops the ancestor walk without being resolved -- `-e'
# fails on one -- which also matches `:A', which leaves
# broken symlinks unresolved.)
#
# Arguments:
# $1 The path to canonicalize
#
# Returns the canonical path in $REPLY.
############################################################
_zshz_realpath() {
local dir=${1:a}
local -a tail
# `:h' at its fixed point (`/', or `//' where the OS treats that as
# distinct) can climb no higher; if even that much of the path does not
# exist, settle for the lexical normalization rather than hand `:A'
# something dangerous.
while [[ ! -e $dir && $dir != "${dir:h}" ]]; do
tail=( "${dir:t}" "${tail[@]}" )
dir=${dir:h}
done
[[ -e $dir ]] && dir=${dir:A}
# `typeset -g': REPLY belongs to the caller by design. A plain assignment
# would trip WARN_NESTED_VAR under `ZSHZ_DEBUG', since _zshz_realpath is a
# top-level function and thus one of the ones `functions -W' marks.
if (( ${#tail} )); then
typeset -g REPLY=${dir%/}/${(j:/:)tail}
else
typeset -g REPLY=$dir
fi
}
# Load zsh/datetime module, if necessary
(( ${+EPOCHSECONDS} )) || zmodload zsh/datetime
# Global associative array for internal use
typeset -gA ZSHZ
# Fallback utilities in case Zsh lacks zsh/files (as is the case with MobaXterm)
ZSHZ[CHMOD]='chmod'
ZSHZ[CHOWN]='chown'
ZSHZ[MV]='mv'
ZSHZ[RM]='rm'
# Try to load zsh/files. zf_chown, zf_mv, and zf_rm are usually present in Zsh
# 4.3.11. zf_chmod only became available in Zsh 5.0, so we load it separately
# below. If zsh/files is not available at all, we silently fall back to the
# external utilities chmod, chown, mv, and rm.
if [[ ${builtins[zf_chown]-} != 'defined' ||
${builtins[zf_mv]-} != 'defined' ||
${builtins[zf_rm]-} != 'defined' ]]; then
zmodload -F zsh/files b:zf_chown b:zf_mv b:zf_rm &> /dev/null
fi
[[ ${builtins[zf_chmod]-} == 'defined' ]] ||
zmodload -F zsh/files b:zf_chmod &> /dev/null
# Use zsh/files, if it is available.
[[ ${builtins[zf_chmod]-} == 'defined' ]] && ZSHZ[CHMOD]='zf_chmod'
[[ ${builtins[zf_chown]-} == 'defined' ]] && ZSHZ[CHOWN]='zf_chown'
[[ ${builtins[zf_mv]-} == 'defined' ]] && ZSHZ[MV]='zf_mv'
[[ ${builtins[zf_rm]-} == 'defined' ]] && ZSHZ[RM]='zf_rm'
# Load zsh/system, if necessary
[[ ${modules[zsh/system]-} == 'loaded' ]] || zmodload zsh/system &> /dev/null
# Make sure ZSHZ_EXCLUDE_DIRS has been declared so that other scripts can
# simply append to it
(( ${+ZSHZ_EXCLUDE_DIRS} )) || typeset -gUa ZSHZ_EXCLUDE_DIRS
# Determine if zsystem flock is available
zsystem supports flock &> /dev/null && ZSHZ[USE_FLOCK]=1
# Windows only: how many times to retry a datafile rename that fails.
#
# On Cygwin and MSYS2, rename() fails with EBUSY or EACCES whenever another
# process holds the tempfile or the datafile open without FILE_SHARE_DELETE --
# which is precisely what a virus scanner or the search indexer does to a file
# in the moments after it is created. Since the write path below creates the
# tempfile and renames it over the datafile microseconds later, that window is
# wide open. The rename's stderr is discarded there, so a scan that lands in
# the window silently loses an `--add' or a `-x': no message, no delay, just a
# directory that never made it into the database. The condition clears in
# milliseconds, so make a few more attempts before giving up.
#
# Everywhere else a failed rename means something real -- ENOSPC, EPERM, a
# cross-device move -- that retrying cannot fix and would only add latency to,
# so ZSHZ[MV_RETRIES] stays unset and the loops below make a single attempt,
# exactly as before.
#
# zsh/zselect provides the sub-second delay between attempts without forking
# /bin/sleep, whose fractional-seconds support is not portable in any case.
# MobaXterm's cut-down Cygwin does not ship zsh/zselect, so there
# ZSHZ[MV_RETRY_DELAY] stays unset and the retries happen back to back -- still
# worth making, since the scanner's handle is often gone by the next attempt.
#
# Four retries at 50ms is deliberately modest rather than generous. The rename
# runs while the lockfile is held, so every millisecond spent retrying is a
# millisecond other writers spend waiting, and they give up after
# ZSHZ_LOCK_TIMEOUT (1s by default) -- silently, since their adds are
# best-effort too. A budget that outlasts a large fraction of that timeout
# would trade one process's lost write for several others'. Measured on MSYS2
# against a handle held open with FILE_SHARE_READ, this recovers renames
# blocked for up to ~0.3s, comfortably more than a scan of a file this small
# takes.
if [[ $OSTYPE == (cygwin|msys) ]]; then
ZSHZ[MV_RETRIES]=4
[[ ${modules[zsh/zselect]-} == 'loaded' ]] || zmodload zsh/zselect &> /dev/null
# In hundredths of a second, per `zselect -t'
[[ ${builtins[zselect]-} == 'defined' ]] && ZSHZ[MV_RETRY_DELAY]=5
fi
############################################################
# The Zsh-z Command
#
# Globals:
# ZSHZ
# ZSHZ_CASE
# ZSHZ_CD
# ZSHZ_COMPLETION
# ZSHZ_DATA
# ZSHZ_DEBUG
# ZSHZ_EXCLUDE_DIRS
# ZSHZ_KEEP_DIRS
# ZSHZ_LOCK_TIMEOUT
# ZSHZ_MAX_SCORE
# ZSHZ_OWNER
#
# Arguments:
# $* Command options and arguments
############################################################
zshz() {
# Don't use `emulate -L zsh' - it breaks PUSHD_IGNORE_DUPS
setopt LOCAL_OPTIONS NO_KSH_ARRAYS NO_SH_WORD_SPLIT EXTENDED_GLOB UNSET
(( ZSHZ_DEBUG )) && setopt LOCAL_OPTIONS WARN_CREATE_GLOBAL
local REPLY
local -a lines
# Allow the user to specify a custom datafile in $ZSHZ_DATA (or legacy $_Z_DATA)
local custom_datafile="${ZSHZ_DATA:-$_Z_DATA}"
# $_zshz_quiet_add marks the automatic bookkeeping add that _zshz_precmd
# runs in a `&!' fork before every prompt (_zshz_precmd declares it `local',
# so it is visible here only through that one call). A fork cannot
# record anything in the parent shell, so it has no way to warn just once:
# an unusable $ZSHZ_DATA would otherwise put the same diagnostic on the
# terminal at every prompt for the life of the shell. Stay quiet on that
# path and leave the complaining to the entry points the user actually
# invoked -- including a hand-typed `z --add', which is not marked and so
# still reports.
local quiet
[[ -n ${_zshz_quiet_add-} ]] && quiet=1
# If a datafile was provided as a standalone file without a directory path
# print a warning and return
if [[ -n ${custom_datafile} && ${custom_datafile} != */* ]]; then
(( quiet )) ||
print "ERROR: You configured a custom Zsh-z datafile (${custom_datafile}), but have not specified its directory." >&2
return 1
fi
# Refuse a symlinked datafile while $ZSHZ_OWNER is set, rather than
# following it. That variable means root is acting for an unprivileged user
# -- the documented `sudo -s' setup -- and the resolution just below
# deliberately dereferences a link, so in that configuration Zsh-z would
# write the database wherever a name inside the user's own home points, with
# root's authority. Nothing has to be raced: the link is planted before the
# privileged shell ever starts. Unprivileged use crosses no such boundary and
# keeps the dereference, which is what makes pointing `.z' at synced storage
# work.
#
# Every component, not just the last. Resolution walks the whole path, so a
# symlinked *parent* redirects it just as effectively: with `link' -> `/etc'
# inside a user's home, a datafile of `~/link/passwd' resolves to
# `/etc/passwd' and root rewrites it.
#
# Judged by who owns each link rather than by its mere presence. Symlinked
# system directories are ordinary -- `/home' -> `/usr/home' on the BSDs,
# `/var' -> `/private/var' on macOS -- and refusing those would break Zsh-z
# under $ZSHZ_OWNER on those systems for nothing. Those are root's; what this
# has to reject is a link an unprivileged owner could have planted. `zstat
# -L' reports the link's own owner rather than its target's, which is the
# distinction `-O' cannot make.
if [[ -n ${ZSHZ_OWNER:-${_Z_OWNER}} ]]; then
local _zshz_df=${custom_datafile:-$HOME/.z}
[[ $_zshz_df == /* ]] || _zshz_df="$PWD/$_zshz_df"
zmodload -F zsh/stat b:zstat 2> /dev/null
local _zshz_pfx _zshz_part _zshz_luid
for _zshz_part in ${(s:/:)_zshz_df}; do
[[ -n $_zshz_part ]] || continue
_zshz_pfx+="/$_zshz_part"
[[ -L $_zshz_pfx ]] || continue
# Without zsh/stat there is no way to tell whose link this is, so refuse
# it rather than guess: this path is privileged by definition.
_zshz_luid=''
(( ${+builtins[zstat]} )) &&
_zshz_luid=$(zstat -L +uid "$_zshz_pfx" 2> /dev/null)
if [[ $_zshz_luid != 0 ]]; then
(( quiet )) ||
print "ERROR: Zsh-z will not follow the symlink ${_zshz_pfx} on the way to its datafile while ZSHZ_OWNER is set." >&2
return 1
fi
done
fi
# If the user specified a datafile, use that or default to ~/.z
# If the datafile is a symlink, it gets dereferenced (except under
# $ZSHZ_OWNER, refused just above). Canonicalized with
# _zshz_realpath rather than a bare `:A', which would segfault Zsh 4.3.11
# on a $ZSHZ_DATA pointing into a missing top-level directory -- at every
# prompt, since this line runs in the backgrounded precmd add.
_zshz_realpath "${custom_datafile:-$HOME/.z}"
local datafile=$REPLY
# Clear REPLY as soon as it is captured: the matching machinery below
# relies on it staying empty until a common root or best match is put in
# it (_zshz_find_common_root only assigns REPLY when it finds a root), so
# a datafile path left in REPLY here would surface as a bogus match.
REPLY=''
# If the datafile is a directory, print a warning and return
if [[ -d $datafile ]]; then
(( quiet )) ||
print "ERROR: Zsh-z's datafile (${datafile}) is a directory." >&2
return 1
fi
# Make sure that the datafile exists before attempting to read it or lock it
# for writing. Create it with 0600 permissions from the first instant (umask
# in a subshell) rather than chmodding it afterward: this creation runs
# before the lock is taken, and on Cygwin/MSYS2 a concurrent writer's rename
# passes through a window in which the datafile is unlinked or delete-
# pending, so any second syscall on the path (chmod) -- or even the creating
# open itself -- can fail spuriously. Append mode (>>) creates the file
# without truncating one that a concurrent writer has just renamed into
# place. The first attempt is silent; if the file still does not exist
# afterward (so no concurrent writer supplied it), retry loudly so that real
# failures (directory permissions, read-only filesystem) reach the user.
[[ -f $datafile ]] || {
mkdir -p "${datafile:h}" &&
( umask 077; : >> "$datafile" ) 2> /dev/null ||
[[ -f $datafile ]] ||
( umask 077; : >> "$datafile" )
# When $ZSHZ_OWNER is set (e.g. under `sudo -s'), hand the freshly created
# file off to that user immediately, so a query-only invocation can't leave
# behind a root-owned .z that the normal-user shell can't read. `-h' so a
# symlink that appeared since the check above is retitled itself rather
# than dereferenced onto its target.
local _owner=${ZSHZ_OWNER:-${_Z_OWNER}}
[[ -n $_owner ]] &&
${ZSHZ[CHOWN]} -h "${_owner}:$(id -ng "${_owner}")" "$datafile"
}
# If the datafile still does not exist, the loud retry above has already
# said why; nothing below -- reading, locking, writing -- can succeed
# without it, and each failure would add its own noise. Bailing out here
# matters most on Zsh 4.3.11, where the failed `$(< $datafile)' reads
# below are fatal to a non-interactive shell.
[[ -f $datafile ]] || return 1
# Bail if we don't own the datafile and $ZSHZ_OWNER is not set
[[ -z ${ZSHZ_OWNER:-${_Z_OWNER}} && -f $datafile && ! -O $datafile ]] &&
return
############################################################
# Add a path to or remove one from the datafile
#
# Globals:
# ZSHZ
# ZSHZ_EXCLUDE_DIRS
# ZSHZ_LOCK_TIMEOUT
# ZSHZ_NO_RESOLVE_SYMLINKS
# ZSHZ_OWNER
#
# Arguments:
# $1 Which action to perform (--add/--remove)
# $2 The path to add
############################################################
_zshz_add_or_remove_path() {
local action=$1
shift
if [[ $action == '--add' ]]; then
# These $HOME / $ZSHZ_EXCLUDE_DIRS guards mirror the ones in
# _zshz_precmd, but they are not redundant: precmd filters $PWD as an
# early-out (skip the background fork), whereas --add is now a public
# entry point and must enforce the same policies as the precmd function.
# Keep both in sync.
# Don't add $HOME
[[ $* == $HOME ]] && return
# Don't track directory trees excluded in $ZSHZ_EXCLUDE_DIRS
local exclude
for exclude in ${(@)ZSHZ_EXCLUDE_DIRS:-${(@)_Z_EXCLUDE_DIRS}}; do
case $* in
${exclude}|${exclude}/*) return ;;
esac
done
fi
# Resolve the directory to be removed, and confirm a full-database wipe,
# *before* taking the lock. Both are independent of the datafile, and the
# confirmation is interactive: holding the lock across a `read -q' the user
# might walk away from would make concurrent writers in other shells time
# out on ZSHZ_LOCK_TIMEOUT and silently drop their adds while the prompt
# sits open. A lock should wrap the read-modify-write, never a question.
local xdir # Directory to be removed
if [[ $action == '--remove' ]]; then
# The target is canonicalized without any existence test: an entry
# whose directory has since been deleted is exactly the one a user most
# wants out of the database. _zshz_realpath resolves a missing path the
# way `:A' resolves one -- and, unlike a bare `:A', cannot segfault Zsh
# 4.3.11 on a path whose top-level component is gone. (The old
# `[[ -d ${...:A} ]]' guard offered no protection there: the `:A'
# expands, and crashes, before `-d' ever sees it.)
if (( ${ZSHZ_NO_RESOLVE_SYMLINKS:-${_Z_NO_RESOLVE_SYMLINKS}} )); then
xdir=${${*:-${PWD}}:a}
else
_zshz_realpath "${*:-${PWD}}"
xdir=$REPLY
fi
# Both branches above yield a non-empty absolute path, and that
# matters: under `-R' an empty $xdir would collapse the subtree filter
# below into `${lines_to_keep:#/**}', which matches every line in the
# datafile and erases the lot -- silently, since the whole-database
# confirmation just below tests for `/' rather than for emptiness. Keep
# this guard in case a future change lets an empty resolution through.
[[ -n $xdir ]] || return 1
if (( ${+opts[-R]} )) && [[ $xdir == '/' ]]; then
if ! read -q "?Delete entire Zsh-z database? "; then
print && return 1
fi
fi
fi
# A temporary file that gets copied over the datafile if all goes well
local tempfile="${datafile}.${RANDOM}" lockfile="${datafile}.lock"
integer lockfd=0
# The no-flock fallback's lock. Deliberately a *different* name from
# $lockfile: a plain file left behind by a flock-capable Zsh would make
# `mkdir' fail forever on the same path, deadlocking every later write.
local lockdir="${datafile}.lock.d"
integer lockdir_held=0
{
# Using zsystem flock
if (( ZSHZ[USE_FLOCK] )); then
# Obtain an exclusive lock on the lockfile.
#
# Locking the datafile directly would not actually serialize concurrent
# writers, since the datafile gets replaced by mv and each new datafile
# has a new inode -- so a separate, stable lockfile is needed.
#
# Bound the lock acquisition (default 1s, override with ZSHZ_LOCK_TIMEOUT)
# so a stuck holder can't stall the backgrounded precmd add or freeze a
# user's foreground `z --add' / `z -x'. Once the holder dies, the kernel
# frees the lock and the next add succeeds automatically -- no manual
# `rm ~/.z.lock' needed.
#
# On timeout we return silently and on purpose: the precmd add is
# best-effort and runs backgrounded (`&!'), so there is nowhere useful
# to report to -- a message would land on the terminal asynchronously,
# mid-keystroke, possibly every prompt. To diagnose a database that has
# stopped updating, run a foreground `z --add .' and check `$?': a
# nonzero status means the write did not happen -- 2 is a lock-
# acquisition timeout (contention, or a raised ZSHZ_LOCK_TIMEOUT is
# still too low), 1 is a permissions or ownership problem (e.g. a stale
# root-owned lockfile left by an earlier `sudo -s' session, or a
# symlinked lockfile refused under $ZSHZ_OWNER).
# Create the lockfile 0600-from-birth and silently (umask in a
# subshell), mirroring the datafile creation above rather than a bare
# `touch' under the ambient umask with unsuppressed stderr. zsystem
# flock opens the lockfile O_RDWR, so under `sudo -s' with $ZSHZ_OWNER
# the unprivileged user must be able to open it: hand it off at
# creation, not only after a successful write -- a timed-out or failed
# first write by root would skip the post-write chown and leave a
# root-owned lockfile, turning every later user --add / -x into a
# silently-swallowed EACCES no-op. The lockfile is deliberately never
# removed: unlinking one a waiter has already opened reintroduces the
# two-inodes race the stable lockfile exists to prevent.
# Under $ZSHZ_OWNER all of this runs with root's authority on a path the
# unprivileged owner controls, and every step follows a symlink: `-f'
# tests the target, `>>' creates a dangling one, and flock opens it.
# $datafile survives a planted link only because the `mv' below replaces
# it outright; the lockfile is deliberately never removed, so a symlink
# here would persist and be acted on at every subsequent write. Refuse.
local _lock_owner=${ZSHZ_OWNER:-${_Z_OWNER}}
[[ -n $_lock_owner && -L $lockfile ]] && return 1
if [[ ! -f $lockfile ]]; then
( umask 077; : >> "$lockfile" ) 2> /dev/null
[[ -n $_lock_owner ]] &&
${ZSHZ[CHOWN]} -h "${_lock_owner}:$(id -ng "${_lock_owner}")" "$lockfile"
fi
zsystem flock -t ${ZSHZ_LOCK_TIMEOUT:-1} -f lockfd "$lockfile" 2> /dev/null || return
else
# No `zsystem flock' here. MobaXterm's cut-down Cygwin is the case that
# matters -- it ships no `zsh/system' at all -- and until now this path
# wrote with nothing serializing it: every writer read its own snapshot
# and the last `mv' won. Measured on MobaXterm, an entry added by one of
# four concurrent writers went missing in 7 runs out of 10.
#
# `mkdir' is the portable atomic primitive: it succeeds for exactly one
# caller and fails for the rest, with no module behind it. What it does
# not give us is the kernel's release-on-death, which is the whole
# reason `flock' is preferred where it exists -- so a holder that dies
# would wedge every later write. Hence the staleness sweep below.
#
# Failure to acquire returns 2, the same status the flock branch's
# timeout produces and the one the README documents for contention.
integer _zshz_deadline=$(( EPOCHSECONDS + ${ZSHZ_LOCK_TIMEOUT:-1} ))
local -a _zshz_stale
while :; do
if mkdir "$lockdir" 2> /dev/null; then
lockdir_held=1
break
fi
# Break a lock nobody can still be holding. A write is a matter of
# milliseconds, so a lock directory older than 30 seconds means its
# owner died without releasing it. `mkdir' stamps the mtime at
# creation and no holder touches it afterwards, so the age is the
# hold time. `$lockdir' expands literally here -- only the qualifier
# is glob syntax -- so a datafile path containing `[' or `*' is safe.
_zshz_stale=( ${lockdir}(Nms+30) )
if (( ${#_zshz_stale} )); then
rmdir "$lockdir" 2> /dev/null && continue
fi
(( EPOCHSECONDS >= _zshz_deadline )) && return 2
# No `zselect' on the platforms that land here, so this costs a fork.
# It is the slow path already, and spinning would be worse.
sleep 0.05 2> /dev/null || :
done
fi
# Read the datafile only after obtaining the lock, so concurrent --add
# calls don't all act on the same stale snapshot.
lines=( ${(f)"$(< $datafile)"} )
# Discard entries that are incomplete or incorrectly formatted
lines=( ${(M)lines:#/*\|[[:digit:]]##[.,]#[[:digit:]]#\|[[:digit:]]##} )
# Hold the fd in an *unset* scalar, not `integer tmpfd' (which seeds it
# with 0). On some Zsh builds, `exec {tmpfd}>|...' refuses to clobber a
# parameter already holding a number that names an open fd -- and 0 is
# stdin, always open -- yielding "can't clobber parameter tmpfd
# containing file descriptor 0". An empty scalar isn't a valid fd, so
# the guard never fires. See https://github.com/agkozak/zsh-z/issues/81
local tmpfd
case $action in
--add)
# When zf_chmod isn't available (Zsh 4.3.11), avoid the
# ~900us fork+execve of external /usr/bin/chmod on every
# write. Create the tempfile with mode 0600 from the start
# via `umask 077' inside a subshell -- the umask change is
# contained to the forked child process and the OS prevents
# it from leaking back to the parent. Subshell fork without
# exec is ~50us, ~18x cheaper than the chmod fallback.
if [[ ${ZSHZ[CHMOD]} == 'zf_chmod' ]]; then
exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
# Fail closed. The tempfile is born with the ambient umask (0666
# under `umask 000'), and it is this inode -- not the datafile's --
# that the rename below publishes, so a chmod whose failure went
# unnoticed would replace a 0600 datafile with a world-readable one
# and still report success. Nothing has been written yet, so
# there is no salvage: drop the tempfile and leave the database as
# it was.
if ! ${ZSHZ[CHMOD]} 600 "$tempfile"; then
exec {tmpfd}>&-
${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
return 1
fi
_zshz_update_datafile $tmpfd "$*"
else
( umask 077
exec {tmpfd}>|"$tempfile"
_zshz_update_datafile $tmpfd "$*" )
fi
local ret=$?
;;
--remove)
# $xdir was resolved before the lock, and for `-xR /' the
# whole-database wipe was already confirmed there.
local -a lines_to_keep
if (( ${+opts[-R]} )); then
# All of the lines that don't match the directory to be deleted
lines_to_keep=( ${lines:#${xdir}\|*} )
# Or its subdirectories
lines_to_keep=( ${lines_to_keep:#${xdir%/}/**} )
else
# All of the lines that don't match the directory to be deleted
lines_to_keep=( ${lines:#${xdir}\|*} )
fi
if [[ $lines != "$lines_to_keep" ]]; then
lines=( $lines_to_keep )
else
return 1 # The $PWD isn't in the datafile
fi
# Same umask-subshell pattern as --add: avoid the external
# chmod when zf_chmod isn't available.
if [[ ${ZSHZ[CHMOD]} == 'zf_chmod' ]]; then
exec {tmpfd}>|"$tempfile" # Open up tempfile for writing
# Fail closed, exactly as on the --add path above.
if ! ${ZSHZ[CHMOD]} 600 "$tempfile"; then
exec {tmpfd}>&-
${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
return 1
fi
# `-r': $lines are verbatim on-disk lines (the datafile stores
# literal paths), so they must be written back unchanged. Without
# `-r', print would collapse an escape -- e.g. a literal `\t' in a
# path into a tab -- silently corrupting bystander entries.
print -u $tmpfd -rl -- $lines
else
( umask 077; print -rl -- $lines >| "$tempfile" )
fi
local ret=$?
;;
esac
if [[ -n $tmpfd ]]; then
# Close tempfile
exec {tmpfd}>&-
fi
if (( ret != 0 )); then
# Avoid clobbering the datafile if the write to tempfile failed
${ZSHZ[RM]} -f "$tempfile"
return $ret
fi
integer write_ret chown_ret mv_attempts
local owner
owner=${ZSHZ_OWNER:-${_Z_OWNER}}
if (( ZSHZ[USE_FLOCK] )); then
# An unusual case: if inside Docker container where datafile could be bind
# mounted
if [[ -f '/.dockerenv' || ( -r '/proc/1/cgroup' && "$(< '/proc/1/cgroup')" == *docker* ) ]]; then
# Secure the datafile *before* its contents land. This branch writes
# in place instead of renaming an already-0600 tempfile over the
# path, so asserting the mode afterwards -- as this did -- leaves a
# bind-mounted datafile that arrived permissive readable for the
# length of the write, and leaves it readable for good if the chmod
# fails and nothing checks. The mode carries across the truncating
# write below, which reuses this same inode.
if ! ${ZSHZ[CHMOD]} 600 "$datafile" 2> /dev/null; then
${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
return 1
fi
# This is the one write path where a symlink at $datafile redirects
# real database content: the sibling branch renames a finished
# tempfile over the path, and a rename *replaces* a link rather than
# writing through it, while `>|' follows one. Under $ZSHZ_OWNER that
# content goes out with root's authority to a path an unprivileged
# owner controls, so a `-L' test ahead of the write is not enough --
# the path can be swapped in between.
#
# `sysopen -o nofollow' settles it atomically, at open time, and the
# write goes through that descriptor. If it is unavailable (Zsh
# 4.3.11 has `zsystem flock' but no `sysopen' at all, and O_NOFOLLOW
# is not universal) or it refuses the open, the privileged write is
# refused rather than retried by a following one: this degrades to
# failing closed, never to writing unsafely. Without an owner set no
# privilege is crossed and the plain redirection stands.
#
# `chmod' above stays path-based -- Zsh has no `fchmod' -- so a swap
# can still misdirect it. Setting the mode on the wrong file is a far
# smaller matter than writing the database into it, and the write is
# what this closes.
local _zshz_dfd
if [[ -n $owner ]]; then
if (( ${+builtins[sysopen]} )) &&
sysopen -o trunc,nofollow -w -u _zshz_dfd "$datafile" 2> /dev/null
then
print -u $_zshz_dfd -r -- "$(< "$tempfile")" 2> /dev/null
write_ret=$?
exec {_zshz_dfd}>&-
else
${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
return 1
fi
else
# `-r': re-emit the tempfile's already-literal contents byte-for-byte.
print -r -- "$(< "$tempfile")" >| "$datafile" 2> /dev/null
write_ret=$?
fi
${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
# All other cases
else
# Retry a rename that a Windows sharing violation turned away; see
# the ZSHZ[MV_RETRIES] comment at the top of this file. Off Windows
# this loop makes the same single attempt it always has. Retrying is
# safe here: the rename happens under the lock, so no other writer
# can slip in between attempts.
while :; do
if ${ZSHZ[MV]} "$tempfile" "$datafile" 2> /dev/null; then
write_ret=0
else
write_ret=$?
fi
(( write_ret == 0 )) && break
(( mv_attempts++ >= ${ZSHZ[MV_RETRIES]:-0} )) && break
if (( ${+ZSHZ[MV_RETRY_DELAY]} )); then
zselect -t ${ZSHZ[MV_RETRY_DELAY]} || :
fi
done
(( write_ret != 0 )) && ${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
fi
# Preserve the write failure itself; best-effort tempfile cleanup must not
# turn a failed persist into a successful return.
(( write_ret == 0 )) || return $write_ret
if [[ -n $owner ]]; then
# Chown the lockfile alongside the datafile: zsystem flock opens it
# O_RDWR, so if root creates it first under sudo -s, the unprivileged
# $ZSHZ_OWNER user's flock attempts would fail with EACCES (silently
# swallowed), turning --add and -x into no-ops.
# `-h' on both: the lockfile is never replaced, so a symlink planted
# there outlives any one write, and $datafile can be relinked in the
# window between the `mv' above and this line. Retitling the link
# itself -- which the owner already owns -- costs nothing, while
# dereferencing hands root's authority to whatever it names.
${ZSHZ[CHOWN]} -h "${owner}:$(id -ng "${owner}")" "$datafile" "$lockfile"
chown_ret=$?
# Surface post-write chown failures too: the current write landed, but a
# wrong owner can break the next locked write.
(( chown_ret == 0 )) || return $chown_ret
fi
else
if [[ -n $owner ]]; then
${ZSHZ[CHOWN]} -h "${owner}:$(id -ng "${owner}")" "$tempfile"
chown_ret=$?
if (( chown_ret != 0 )); then
# In the no-flock path, chown happens before the move, so clean up the
# tempfile and leave the live database untouched.
${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
return $chown_ret
fi
fi
# Same Windows sharing-violation retry as the flock branch above. This
# path is the one MobaXterm's cut-down Cygwin takes, and it has neither
# zsystem flock nor zsh/zselect, so the retries there run back to back.
while :; do
if ${ZSHZ[MV]} -f "$tempfile" "$datafile" 2> /dev/null; then
write_ret=0
else
write_ret=$?
fi
(( write_ret == 0 )) && break
(( mv_attempts++ >= ${ZSHZ[MV_RETRIES]:-0} )) && break
if (( ${+ZSHZ[MV_RETRY_DELAY]} )); then
zselect -t ${ZSHZ[MV_RETRY_DELAY]} || :
fi
done
if (( write_ret != 0 )); then
${ZSHZ[RM]} -f "$tempfile" 2> /dev/null
return $write_ret
fi
fi
} always {
# zsystem flock -f opens a real fd; explicitly unlock it so repeated
# foreground `z --add' / `z -x' invocations in the interactive shell
# don't leak lock descriptors and stall peers. (A backgrounded precmd
# child releases its fd on exit regardless; this matters for the parent.)
(( lockfd != 0 )) && zsystem flock -u $lockfd 2> /dev/null
# Release the mkdir lock on every exit from the block above, including
# the early `return's -- unlike an fd, a directory outlives the process
# that made it, so a missed release here is a wedged database rather than
# a leaked descriptor. Only if this call is the one that took it.
(( lockdir_held )) && rmdir "$lockdir" 2> /dev/null
}
# In order to make z -x work, we have to disable zsh-z's adding
# to the database until the user changes directory and the
# chpwd_functions are run
if [[ $action == '--remove' ]]; then
ZSHZ[DIRECTORY_REMOVED]=1
fi
}
############################################################
# Read the current datafile contents, update them, "age" them
# when the total rank gets high enough, and print the new
# contents to STDOUT.
#
# Globals:
# ZSHZ_KEEP_DIRS
# ZSHZ_MAX_SCORE
#
# Arguments:
# $1 File descriptor linked to tempfile
# $2 Path to be added to datafile
############################################################
_zshz_update_datafile() {
integer fd=$1
local -A rank time
# Characters special to the shell (such as '[]') are quoted with backslashes
# See https://github.com/rupa/z/issues/246
local add_path=${(q)2}
local now=$EPOCHSECONDS line dir
local path_field rank_field time_field count x
local -i keep
rank[$add_path]=1
time[$add_path]=$now
for line in $lines; do
path_field=${line%%\|*}
# Filter non-existent paths (honoring ZSHZ_KEEP_DIRS) inline so
# we walk $lines once instead of twice. The `keep=1; break' also
# fixes a latent bug: the previous existence-check loop had no
# `break' after appending, so a non-existent path matching
# multiple ZSHZ_KEEP_DIRS patterns was processed more than once.
if [[ ! -d $path_field ]]; then
keep=0
for dir in ${(@)ZSHZ_KEEP_DIRS}; do
if [[ $path_field == ${dir}/* || $path_field == $dir || $dir == '/' ]]; then
keep=1
break
fi
done
(( keep )) || continue
fi
# Quote in place: assoc-array keys need shell-special chars
# backslash-escaped (rupa/z#246).
path_field=${(q)path_field}
rank_field=${${line%\|*}#*\|}
time_field=${line##*\|}
# When a rank drops below 1, drop the path from the database
(( rank_field < 1 )) && continue
if [[ $path_field == $add_path ]]; then
# Compute the new rank with a scalar expression, not `(( rank[$key]++ ))'.
# The keys are `${(q)}'-quoted (rupa/z#246); a math-context subscript
# runs its key through the arithmetic lexer, which strips a backslash
# level and so misses any key containing `$ \ [ ] ( )' or a backtick --
# incrementing a phantom raw-keyed entry and leaving the real one stuck.
# An assignment subscript expands the key literally, so it is safe.
rank[$path_field]=$(( rank_field + 1 ))
time[$path_field]=$now
else
rank[$path_field]=$rank_field
time[$path_field]=$time_field
fi
(( count += rank_field ))
done
local -a out
if (( count > ${ZSHZ_MAX_SCORE:-${_Z_MAX_SCORE:-9000}} )); then
# Aging
for x in ${(k)rank}; do
# `${rank[$x]}', not a bare `rank[$x]' math subscript: the keys are
# `${(q)}'-quoted (rupa/z#246), and a math-context subscript would run
# the key through the arithmetic lexer, stripping a backslash level and
# missing any key with `$ \ [ ] ( )' or a backtick -- yielding 0, which
# the `rank_field < 1' drop above then erases on the next write. The
# expansion substitutes the numeric value before the math parser runs.
out+=( "$x|$(( 0.99 * ${rank[$x]} ))|${time[$x]}" )
done
else
for x in ${(k)rank}; do
out+=( "$x|${rank[$x]}|${time[$x]}" )
done
fi
# Deliberately NO `-r' here, unlike every other datafile write. The keys in
# $out are `${(q)}'-quoted (assoc-array keys need shell-special chars
# backslash-escaped -- rupa/z#246), and a plain `print' strips exactly one
# backslash level back off, so what lands on disk is the literal path the
# rest of the code expects. Adding `-r' would store the still-quoted form
# (e.g. `/foo\ bar'), which the read path -- it does not unquote -- would
# then fail to match. The verbatim-passthrough writes in
# `_zshz_add_or_remove_path' DO use `-r' because their input is already
# literal; this one is not.
print -u $fd -l -- $out || return 1
}
############################################################
# The original tab completion method
#
# String processing is smartcase -- case-insensitive if the
# search string is lowercase, case-sensitive if there are
# any uppercase letters. Spaces in the search string are
# treated as *'s in globbing. Read the contents of the
# datafile and print matches to STDOUT.
#
# Arguments:
# $1 The string to be completed
############################################################
_zshz_legacy_complete() {
local line path_field path_field_normalized
# Replace spaces in the search string with asterisks for globbing
1=${1//[[:space:]]/*}
# Hoist loop-invariants out of the per-line loop -- $1 and
# $ZSHZ_TRAILING_SLASH don't change inside the loop, so the
# lowercase comparison and the trailing-slash branch were pure
# waste when recomputed N times. `query_lower' lets the case-
# insensitive branch glob against a precompiled lowercase pattern.
local query_lower=${1:l}
local -i is_lowercase_query=0
[[ $1 == $query_lower ]] && is_lowercase_query=1
local -i trail=${ZSHZ_TRAILING_SLASH:-0}
for line in $lines; do
path_field=${line%%\|*}
path_field_normalized=$path_field
(( trail )) && path_field_normalized=${path_field%/}/
# If the search string is all lowercase, the search will be case-insensitive
if (( is_lowercase_query )) && [[ ${path_field_normalized:l} == *${~query_lower}* ]]; then
print -r -- $path_field
# Otherwise, case-sensitive
elif [[ $path_field_normalized == *${~1}* ]]; then
print -r -- $path_field
fi
done
# TODO: Search strings with spaces in them are currently treated case-
# insensitively.
}
############################################################
# If matches share a common root, find it, and put it in
# REPLY for _zshz_output to use.
#
# Arguments:
# $@ Candidate paths
############################################################
_zshz_find_common_root() {
local -a common_matches
local x short
common_matches=( "$@" )
for x in ${(@)common_matches}; do
if [[ -z $short ]] || (( $#x < $#short )) || [[ $x != ${short}/* ]]; then
short=$x
fi
done
[[ $short == '/' ]] && return
for x in ${(@)common_matches}; do
[[ $x != $short* ]] && return
done