1# Copyright (c) 2009 rupa deadwyler. Licensed under the WTFPL license, Version 2 2 3# maintains a jump-list of the directories you actually use 4# 5# INSTALL: 6# * put something like this in your .bashrc/.zshrc: 7# . /path/to/z.sh 8# * cd around for a while to build up the db 9# * PROFIT!! 10# * optionally: 11# set $_Z_CMD in .bashrc/.zshrc to change the command (default z). 12# set $_Z_DATA in .bashrc/.zshrc to change the datafile (default ~/.z). 13# set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution. 14# set $_Z_NO_PROMPT_COMMAND if you're handling PROMPT_COMMAND yourself. 15# set $_Z_EXCLUDE_DIRS to an array of directories to exclude. 16# set $_Z_OWNER to your username if you want use z while sudo with $HOME kept 17# 18# USE: 19# * z foo # cd to most frecent dir matching foo 20# * z foo bar # cd to most frecent dir matching foo and bar 21# * z -r foo # cd to highest ranked dir matching foo 22# * z -t foo # cd to most recently accessed dir matching foo 23# * z -l foo # list matches instead of cd 24# * z -c foo # restrict matches to subdirs of $PWD 25 26[ -d "${_Z_DATA:-$HOME/.z}" ] && { 27 echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory." 28} 29 30_z() { 31 32 local datafile="${_Z_DATA:-$HOME/.z}" 33 34 # bail if we don't own ~/.z and $_Z_OWNER not set 35 [ -z "$_Z_OWNER" -a -f "$datafile" -a ! -O "$datafile" ] && return 36 37 _z_dirs () { 38 while read line; do 39 # only count directories 40 [ -d "${line%%\|*}" ] && echo $line 41 done < "$datafile" 42 return 0 43 } 44 45 # add entries 46 if [ "$1" = "--add" ]; then 47 shift 48 49 # $HOME isn't worth matching 50 [ "$*" = "$HOME" ] && return 51 52 # don't track excluded directory trees 53 local exclude 54 for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do 55 case "$*" in "$exclude*") return;; esac 56 done 57 58 # maintain the data file 59 local tempfile="$datafile.$RANDOM" 60 awk < <(_z_dirs 2>/dev/null) -v path="$*" -v now="$(date +%s)" -F"|" ' 61 BEGIN { 62 rank[path] = 1 63 time[path] = now 64 } 65 $2 >= 1 { 66 # drop ranks below 1 67 if( $1 == path ) { 68 rank[$1] = $2 + 1 69 time[$1] = now 70 } else { 71 rank[$1] = $2 72 time[$1] = $3 73 } 74 count += $2 75 } 76 END { 77 if( count > 9000 ) { 78 # aging 79 for( x in rank ) print x "|" 0.99*rank[x] "|" time[x] 80 } else for( x in rank ) print x "|" rank[x] "|" time[x] 81 } 82 ' 2>/dev/null >| "$tempfile" 83 # do our best to avoid clobbering the datafile in a race condition. 84 if [ $? -ne 0 -a -f "$datafile" ]; then 85 env rm -f "$tempfile" 86 else 87 [ "$_Z_OWNER" ] && chown $_Z_OWNER:$(id -ng $_Z_OWNER) "$tempfile" 88 env mv -f "$tempfile" "$datafile" || env rm -f "$tempfile" 89 fi 90 91 # tab completion 92 elif [ "$1" = "--complete" -a -s "$datafile" ]; then 93 while read line; do 94 [ -d "${line%%\|*}" ] && echo $line 95 done < "$datafile" | awk -v q="$2" -F"|" ' 96 BEGIN { 97 if( q == tolower(q) ) imatch = 1 98 q = substr(q, 3) 99 gsub(" ", ".*", q) 100 } 101 { 102 if( imatch ) { 103 if( tolower($1) ~ tolower(q) ) print $1 104 } else if( $1 ~ q ) print $1 105 } 106 ' 2>/dev/null 107 108 else 109 # list/go 110 while [ "$1" ]; do case "$1" in 111 --) while [ "$1" ]; do shift; local fnd="$fnd${fnd:+ }$1";done;; 112 -*) local opt=${1:1}; while [ "$opt" ]; do case ${opt:0:1} in 113 c) local fnd="^$PWD $fnd";; 114 e) local echo=echo;; 115 h) echo "${_Z_CMD:-z} [-cehlrtx] args" >&2; return;; 116 l) local list=1;; 117 r) local typ="rank";; 118 t) local typ="recent";; 119 x) sed -i -e "\:^${PWD}|.*:d" "$datafile";; 120 esac; opt=${opt:1}; done;; 121 *) local fnd="$fnd${fnd:+ }$1";; 122 esac; local last=$1; [ "$#" -gt 0 ] && shift; done 123 [ "$fnd" -a "$fnd" != "^$PWD " ] || local list=1 124 125 # if we hit enter on a completion just go there 126 case "$last" in 127 # completions will always start with / 128 /*) [ -z "$list" -a -d "$last" ] && builtin cd "$last" && return;; 129 esac 130 131 # no file yet 132 [ -f "$datafile" ] || return 133 134 local cd 135 cd="$( < <( _z_dirs ) awk -v t="$(date +%s)" -v list="$list" -v typ="$typ" -v q="$fnd" -F"|" ' 136 function frecent(rank, time) { 137 # relate frequency and time 138 dx = t - time 139 if( dx < 3600 ) return rank * 4 140 if( dx < 86400 ) return rank * 2 141 if( dx < 604800 ) return rank / 2 142 return rank / 4 143 } 144 function output(files, out, common) { 145 # list or return the desired directory 146 if( list ) { 147 cmd = "sort -n >&2" 148 for( x in files ) { 149 if( files[x] ) printf "%-10s %s\n", files[x], x | cmd 150 } 151 if( common ) { 152 printf "%-10s %s\n", "common:", common > "/dev/stderr" 153 } 154 } else { 155 if( common ) out = common 156 print out 157 } 158 } 159 function common(matches) { 160 # find the common root of a list of matches, if it exists 161 for( x in matches ) { 162 if( matches[x] && (!short || length(x) < length(short)) ) { 163 short = x 164 } 165 } 166 if( short == "/" ) return 167 # use a copy to escape special characters, as we want to return 168 # the original. yeah, this escaping is awful. 169 clean_short = short 170 gsub(/\[\(\)\[\]\|\]/, "\\\\&", clean_short) 171 for( x in matches ) if( matches[x] && x !~ clean_short ) return 172 return short 173 } 174 BEGIN { 175 gsub(" ", ".*", q) 176 hi_rank = ihi_rank = -9999999999 177 } 178 { 179 if( typ == "rank" ) { 180 rank = $2 181 } else if( typ == "recent" ) { 182 rank = $3 - t 183 } else rank = frecent($2, $3) 184 if( $1 ~ q ) { 185 matches[$1] = rank 186 } else if( tolower($1) ~ tolower(q) ) imatches[$1] = rank 187 if( matches[$1] && matches[$1] > hi_rank ) { 188 best_match = $1 189 hi_rank = matches[$1] 190 } else if( imatches[$1] && imatches[$1] > ihi_rank ) { 191 ibest_match = $1 192 ihi_rank = imatches[$1] 193 } 194 } 195 END { 196 # prefer case sensitive 197 if( best_match ) { 198 output(matches, best_match, common(matches)) 199 } else if( ibest_match ) { 200 output(imatches, ibest_match, common(imatches)) 201 } 202 } 203 ')" 204 205 [ $? -eq 0 ] && [ "$cd" ] && { 206 if [ "$echo" ]; then echo "$cd"; else builtin cd "$cd"; fi 207 } 208 fi 209} 210 211alias ${_Z_CMD:-z}='_z 2>&1' 212 213[ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P" 214 215if type compctl >/dev/null 2>&1; then 216 # zsh 217 [ "$_Z_NO_PROMPT_COMMAND" ] || { 218 # populate directory list, avoid clobbering any other precmds. 219 if [ "$_Z_NO_RESOLVE_SYMLINKS" ]; then 220 _z_precmd() { 221 (_z --add "${PWD:a}" &) 222 } 223 else 224 _z_precmd() { 225 (_z --add "${PWD:A}" &) 226 } 227 fi 228 [[ -n "${precmd_functions[(r)_z_precmd]}" ]] || { 229 precmd_functions[$(($#precmd_functions+1))]=_z_precmd 230 } 231 } 232 _z_zsh_tab_completion() { 233 # tab completion 234 local compl 235 read -l compl 236 reply=(${(f)"$(_z --complete "$compl")"}) 237 } 238 compctl -U -K _z_zsh_tab_completion _z 239elif type complete >/dev/null 2>&1; then 240 # bash 241 # tab completion 242 complete -o filenames -C '_z --complete "$COMP_LINE"' ${_Z_CMD:-z} 243 [ "$_Z_NO_PROMPT_COMMAND" ] || { 244 # populate directory list. avoid clobbering other PROMPT_COMMANDs. 245 grep "_z --add" <<< "$PROMPT_COMMAND" >/dev/null || { 246 PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''(_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null &);' 247 } 248 } 249fi 250