bin/z.sh (view raw)
1#!/usr/bin/env bash
2[ -d "${_Z_DATA:-$HOME/.z}" ] && {
3 echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory."
4}
5
6_z() {
7
8 local datafile="${_Z_DATA:-$HOME/.z}"
9
10 # if symlink, dereference
11 [ -h "$datafile" ] && datafile=$(readlink "$datafile")
12
13 # bail if we don't own ~/.z and $_Z_OWNER not set
14 [ -z "$_Z_OWNER" -a -f "$datafile" -a ! -O "$datafile" ] && return
15
16 _z_dirs () {
17 [ -f "$datafile" ] || return
18
19 local line
20 while read line; do
21 # only count directories
22 [ -d "${line%%\|*}" ] && echo "$line"
23 done < "$datafile"
24 return 0
25 }
26
27 # add entries
28 if [ "$1" = "--add" ]; then
29 shift
30
31 # $HOME and / aren't worth matching
32 [ "$*" = "$HOME" -o "$*" = '/' ] && return
33
34 # don't track excluded directory trees
35 if [ ${#_Z_EXCLUDE_DIRS[@]} -gt 0 ]; then
36 local exclude
37 for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do
38 case "$*" in "$exclude"*) return;; esac
39 done
40 fi
41
42 # maintain the data file
43 local tempfile="$datafile.$RANDOM"
44 local score=${_Z_MAX_SCORE:-9000}
45 _z_dirs | awk -v path="$*" -v now="$(date +%s)" -v score=$score -F"|" '
46 BEGIN {
47 rank[path] = 1
48 time[path] = now
49 }
50 $2 >= 1 {
51 # drop ranks below 1
52 if( $1 == path ) {
53 rank[$1] = $2 + 1
54 time[$1] = now
55 } else {
56 rank[$1] = $2
57 time[$1] = $3
58 }
59 count += $2
60 }
61 END {
62 if( count > score ) {
63 # aging
64 for( x in rank ) print x "|" 0.99*rank[x] "|" time[x]
65 } else for( x in rank ) print x "|" rank[x] "|" time[x]
66 }
67 ' 2>/dev/null >| "$tempfile"
68 # do our best to avoid clobbering the datafile in a race condition.
69 if [ $? -ne 0 -a -f "$datafile" ]; then
70 env rm -f "$tempfile"
71 else
72 [ "$_Z_OWNER" ] && chown $_Z_OWNER:"$(id -ng $_Z_OWNER)" "$tempfile"
73 env mv -f "$tempfile" "$datafile" || env rm -f "$tempfile"
74 fi
75
76 # tab completion
77 elif [ "$1" = "--complete" -a -s "$datafile" ]; then
78 _z_dirs | awk -v q="$2" -F"|" '
79 BEGIN {
80 q = substr(q, 3)
81 if( q == tolower(q) ) imatch = 1
82 gsub(/ /, ".*", q)
83 }
84 {
85 if( imatch ) {
86 if( tolower($1) ~ q ) print $1
87 } else if( $1 ~ q ) print $1
88 }
89 ' 2>/dev/null
90
91 else
92 # list/go
93 local echo fnd last list opt typ
94 while [ "$1" ]; do case "$1" in
95 --) while [ "$1" ]; do shift; fnd="$fnd${fnd:+ }$1";done;;
96 -*) opt=${1:1}; while [ "$opt" ]; do case ${opt:0:1} in
97 c) fnd="^$PWD $fnd";;
98 e) echo=1;;
99 h) echo "${_Z_CMD:-z} [-cehlrtx] args" >&2; return;;
100 l) list=1;;
101 r) typ="rank";;
102 t) typ="recent";;
103 x) sed -i -e "\:^${PWD}|.*:d" "$datafile";;
104 esac; opt=${opt:1}; done;;
105 *) fnd="$fnd${fnd:+ }$1";;
106 esac; last=$1; [ "$#" -gt 0 ] && shift; done
107 [ "$fnd" -a "$fnd" != "^$PWD " ] || list=1
108
109 # if we hit enter on a completion just go there
110 case "$last" in
111 # completions will always start with /
112 /*) [ -z "$list" -a -d "$last" ] && builtin cd "$last" && return;;
113 esac
114
115 # no file yet
116 [ -f "$datafile" ] || return
117
118 local cd
119 cd="$( < <( _z_dirs ) awk -v t="$(date +%s)" -v list="$list" -v typ="$typ" -v q="$fnd" -F"|" '
120 function frecent(rank, time) {
121 # relate frequency and time
122 dx = t - time
123 return int(10000 * rank * (3.75/((0.0001 * dx + 1) + 0.25)))
124 }
125 function output(matches, best_match, common) {
126 # list or return the desired directory
127 if( list ) {
128 if( common ) {
129 printf "%-10s %s\n", "common:", common > "/dev/stderr"
130 }
131 cmd = "sort -n >&2"
132 for( x in matches ) {
133 if( matches[x] ) {
134 printf "%-10s %s\n", matches[x], x | cmd
135 }
136 }
137 } else {
138 if( common && !typ ) best_match = common
139 print best_match
140 }
141 }
142 function common(matches) {
143 # find the common root of a list of matches, if it exists
144 for( x in matches ) {
145 if( matches[x] && (!short || length(x) < length(short)) ) {
146 short = x
147 }
148 }
149 if( short == "/" ) return
150 for( x in matches ) if( matches[x] && index(x, short) != 1 ) {
151 return
152 }
153 return short
154 }
155 BEGIN {
156 gsub(" ", ".*", q)
157 hi_rank = ihi_rank = -9999999999
158 }
159 {
160 if( typ == "rank" ) {
161 rank = $2
162 } else if( typ == "recent" ) {
163 rank = $3 - t
164 } else rank = frecent($2, $3)
165 if( $1 ~ q ) {
166 matches[$1] = rank
167 } else if( tolower($1) ~ tolower(q) ) imatches[$1] = rank
168 if( matches[$1] && matches[$1] > hi_rank ) {
169 best_match = $1
170 hi_rank = matches[$1]
171 } else if( imatches[$1] && imatches[$1] > ihi_rank ) {
172 ibest_match = $1
173 ihi_rank = imatches[$1]
174 }
175 }
176 END {
177 # prefer case sensitive
178 if( best_match ) {
179 output(matches, best_match, common(matches))
180 exit
181 } else if( ibest_match ) {
182 output(imatches, ibest_match, common(imatches))
183 exit
184 }
185 exit(1)
186 }
187 ')"
188
189 if [ "$?" -eq 0 ]; then
190 if [ "$cd" ]; then
191 if [ "$echo" ]; then echo "$cd"; else builtin cd "$cd"; fi
192 fi
193 else
194 return $?
195 fi
196 fi
197}
198
199alias ${_Z_CMD:-z}='_z 2>&1'
200
201[ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P"
202
203if type complete >/dev/null 2>&1; then
204 # bash
205 # tab completion
206 complete -o filenames -C '_z --complete "$COMP_LINE"' ${_Z_CMD:-z}
207 [ "$_Z_NO_PROMPT_COMMAND" ] || {
208 # populate directory list. avoid clobbering other PROMPT_COMMANDs.
209 grep "_z --add" <<< "$PROMPT_COMMAND" >/dev/null || {
210 PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''(_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null &);'
211 }
212 }
213fi