[download]

bash/z.sh

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