Browse Source

Merge pull request #4181 from posva/z

Update z version to 5dc2a86
Marc Cornellà 9 years ago
parent
commit
85d949550b
3 changed files with 269 additions and 234 deletions
  1. 26 17
      plugins/z/README
  2. 23 12
      plugins/z/z.1
  3. 220 205
      plugins/z/z.sh

+ 26 - 17
plugins/z/README

@@ -6,7 +6,7 @@ NAME
        z - jump around
        z - jump around
 
 
 SYNOPSIS
 SYNOPSIS
-       z [-chlrt] [regex1 regex2 ... regexn]
+       z [-chlrtx] [regex1 regex2 ... regexn]
 
 
 AVAILABILITY
 AVAILABILITY
        bash, zsh
        bash, zsh
@@ -15,10 +15,13 @@ DESCRIPTION
        Tracks your most used directories, based on 'frecency'.
        Tracks your most used directories, based on 'frecency'.
 
 
        After  a  short  learning  phase, z will take you to the most 'frecent'
        After  a  short  learning  phase, z will take you to the most 'frecent'
-       directory that matches ALL of the regexes given on the command line.
+       directory that matches ALL of the regexes given on the command line, in
+       order.
+
+       For example, z foo bar would match /foo/bar but not /bar/foo.
 
 
 OPTIONS
 OPTIONS
-       -c     restrict matches to subdirectories of the current directory.
+       -c     restrict matches to subdirectories of the current directory
 
 
        -h     show a brief help message
        -h     show a brief help message
 
 
@@ -28,10 +31,12 @@ OPTIONS
 
 
        -t     match by recent access only
        -t     match by recent access only
 
 
+       -x     remove the current directory from the datafile
+
 EXAMPLES
 EXAMPLES
        z foo         cd to most frecent dir matching foo
        z foo         cd to most frecent dir matching foo
 
 
-       z foo bar     cd to most frecent dir matching foo and bar
+       z foo bar     cd to most frecent dir matching foo, then bar
 
 
        z -r foo      cd to highest ranked dir matching foo
        z -r foo      cd to highest ranked dir matching foo
 
 
@@ -55,8 +60,9 @@ NOTES
               Set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution.
               Set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution.
               Set $_Z_NO_PROMPT_COMMAND to handle PROMPT_COMMAND/precmd  your-
               Set $_Z_NO_PROMPT_COMMAND to handle PROMPT_COMMAND/precmd  your-
               self.
               self.
-              Set $_Z_EXCLUDE_DIRS to an array of directories to exclude.
-              (These  settings  should  go  in .bashrc/.zshrc before the lines
+              Set $_Z_EXCLUDE_DIRS to an array of directory trees to  exclude.
+              Set $_Z_OWNER to allow usage when in 'sudo -s' mode.
+              (These  settings  should  go  in  .bashrc/.zshrc before the line
               added above.)
               added above.)
               Install   the   provided   man   page   z.1    somewhere    like
               Install   the   provided   man   page   z.1    somewhere    like
               /usr/local/man/man1.
               /usr/local/man/man1.
@@ -64,12 +70,12 @@ NOTES
    Aging:
    Aging:
        The rank of directories maintained by z undergoes aging based on a sim-
        The rank of directories maintained by z undergoes aging based on a sim-
        ple formula. The rank of each entry is incremented  every  time  it  is
        ple formula. The rank of each entry is incremented  every  time  it  is
-       accessed.  When  the  sum  of ranks is greater than 6000, all ranks are
-       multiplied by 0.99. Entries with a rank lower than 1 are forgotten.
+       accessed.  When the sum of ranks is over 9000, all ranks are multiplied
+       by 0.99. Entries with a rank lower than 1 are forgotten.
 
 
    Frecency:
    Frecency:
-       Frecency is a portmantaeu of 'recent' and 'frequency'. It is a weighted
-       rank  that  depends on how often and how recently something occured. As
+       Frecency is a portmanteau of 'recent' and 'frequency'. It is a weighted
+       rank  that depends on how often and how recently something occurred. As
        far as I know, Mozilla came up with the term.
        far as I know, Mozilla came up with the term.
 
 
        To z, a directory that has low ranking but has been  accessed  recently
        To z, a directory that has low ranking but has been  accessed  recently
@@ -107,20 +113,23 @@ ENVIRONMENT
        resolving  of  symlinks.  If  it  is  not  set,  symbolic links will be
        resolving  of  symlinks.  If  it  is  not  set,  symbolic links will be
        resolved when added to the datafile.
        resolved when added to the datafile.
 
 
-       In bash, z prepends a command to the PROMPT_COMMAND  environment  vari-
-       able  to  maintain its database. In zsh, z appends a function _z_precmd
-       to the precmd_functions array.
+       In bash, z appends a command to the PROMPT_COMMAND environment variable
+       to maintain its database. In zsh, z appends a function _z_precmd to the
+       precmd_functions array.
 
 
        The environment variable $_Z_NO_PROMPT_COMMAND can be set if  you  want
        The environment variable $_Z_NO_PROMPT_COMMAND can be set if  you  want
        to handle PROMPT_COMMAND or precmd yourself.
        to handle PROMPT_COMMAND or precmd yourself.
 
 
        The  environment  variable  $_Z_EXCLUDE_DIRS  can be set to an array of
        The  environment  variable  $_Z_EXCLUDE_DIRS  can be set to an array of
-       directories to exclude from tracking. $HOME is always excluded.  Direc-
-       tories must be full paths without trailing slashes.
+       directory trees to exclude from tracking.  $HOME  is  always  excluded.
+       Directories must be full paths without trailing slashes.
+
+       The  environment  variable  $_Z_OWNER  can  be set to your username, to
+       allow usage of z when your sudo enviroment keeps $HOME set.
 
 
 FILES
 FILES
-       Data  is  stored  in  $HOME/.z.  This  can be overridden by setting the
-       $_Z_DATA environment variable. When initialized, z will raise an  error
+       Data is stored in $HOME/.z. This  can  be  overridden  by  setting  the
+       $_Z_DATA  environment variable. When initialized, z will raise an error
        if this path is a directory, and not function correctly.
        if this path is a directory, and not function correctly.
 
 
        A man page (z.1) is provided.
        A man page (z.1) is provided.

+ 23 - 12
plugins/z/z.1

@@ -4,7 +4,7 @@ NAME
 z \- jump around
 z \- jump around
 .SH
 .SH
 SYNOPSIS
 SYNOPSIS
-z [\-chlrt] [regex1 regex2 ... regexn]
+z [\-chlrtx] [regex1 regex2 ... regexn]
 .SH
 .SH
 AVAILABILITY
 AVAILABILITY
 bash, zsh
 bash, zsh
@@ -13,12 +13,14 @@ DESCRIPTION
 Tracks your most used directories, based on 'frecency'.
 Tracks your most used directories, based on 'frecency'.
 .P
 .P
 After a short learning phase, \fBz\fR will take you to the most 'frecent'
 After a short learning phase, \fBz\fR will take you to the most 'frecent'
-directory that matches ALL of the regexes given on the command line.
+directory that matches ALL of the regexes given on the command line, in order.
+
+For example, \fBz foo bar\fR would match \fB/foo/bar\fR but not \fB/bar/foo\fR.
 .SH
 .SH
 OPTIONS
 OPTIONS
 .TP
 .TP
 \fB\-c\fR
 \fB\-c\fR
-restrict matches to subdirectories of the current directory.
+restrict matches to subdirectories of the current directory
 .TP
 .TP
 \fB\-h\fR
 \fB\-h\fR
 show a brief help message
 show a brief help message
@@ -31,13 +33,16 @@ match by rank only
 .TP
 .TP
 \fB\-t\fR
 \fB\-t\fR
 match by recent access only
 match by recent access only
+.TP
+\fB\-x\fR
+remove the current directory from the datafile
 .SH EXAMPLES
 .SH EXAMPLES
 .TP 14
 .TP 14
 \fBz foo\fR
 \fBz foo\fR
 cd to most frecent dir matching foo
 cd to most frecent dir matching foo
 .TP 14
 .TP 14
 \fBz foo bar\fR
 \fBz foo bar\fR
-cd to most frecent dir matching foo and bar
+cd to most frecent dir matching foo, then bar
 .TP 14
 .TP 14
 \fBz -r foo\fR
 \fBz -r foo\fR
 cd to highest ranked dir matching foo
 cd to highest ranked dir matching foo
@@ -76,10 +81,13 @@ Set \fB$_Z_NO_RESOLVE_SYMLINKS\fR to prevent symlink resolution.
 Set \fB$_Z_NO_PROMPT_COMMAND\fR to handle \fBPROMPT_COMMAND/precmd\fR yourself.
 Set \fB$_Z_NO_PROMPT_COMMAND\fR to handle \fBPROMPT_COMMAND/precmd\fR yourself.
 .RE
 .RE
 .RS
 .RS
-Set \fB$_Z_EXCLUDE_DIRS\fR to an array of directories to exclude.
+Set \fB$_Z_EXCLUDE_DIRS\fR to an array of directory trees to exclude.
+.RE
+.RS
+Set \fB$_Z_OWNER\fR to allow usage when in 'sudo -s' mode.
 .RE
 .RE
 .RS
 .RS
-(These settings should go in .bashrc/.zshrc before the lines added above.)
+(These settings should go in .bashrc/.zshrc before the line added above.)
 .RE
 .RE
 .RS
 .RS
 Install the provided man page \fBz.1\fR somewhere like \fB/usr/local/man/man1\fR.
 Install the provided man page \fBz.1\fR somewhere like \fB/usr/local/man/man1\fR.
@@ -88,12 +96,12 @@ Install the provided man page \fBz.1\fR somewhere like \fB/usr/local/man/man1\fR
 Aging:
 Aging:
 The rank of directories maintained by \fBz\fR undergoes aging based on a simple
 The rank of directories maintained by \fBz\fR undergoes aging based on a simple
 formula. The rank of each entry is incremented every time it is accessed. When
 formula. The rank of each entry is incremented every time it is accessed. When
-the sum of ranks is greater than 6000, all ranks are multiplied by 0.99. Entries
-with a rank lower than 1 are forgotten.
+the sum of ranks is over 9000, all ranks are multiplied by 0.99. Entries with a
+rank lower than 1 are forgotten.
 .SS
 .SS
 Frecency:
 Frecency:
-Frecency is a portmantaeu of 'recent' and 'frequency'. It is a weighted rank
-that depends on how often and how recently something occured. As far as I
+Frecency is a portmanteau of 'recent' and 'frequency'. It is a weighted rank
+that depends on how often and how recently something occurred. As far as I
 know, Mozilla came up with the term.
 know, Mozilla came up with the term.
 .P
 .P
 To \fBz\fR, a directory that has low ranking but has been accessed recently
 To \fBz\fR, a directory that has low ranking but has been accessed recently
@@ -131,7 +139,7 @@ The environment variable \fB$_Z_NO_RESOLVE_SYMLINKS\fR can be set to prevent
 resolving of symlinks. If it is not set, symbolic links will be resolved when
 resolving of symlinks. If it is not set, symbolic links will be resolved when
 added to the datafile.
 added to the datafile.
 .P
 .P
-In bash, \fBz\fR prepends a command to the \fBPROMPT_COMMAND\fR environment
+In bash, \fBz\fR appends a command to the \fBPROMPT_COMMAND\fR environment
 variable to maintain its database. In zsh, \fBz\fR appends a function
 variable to maintain its database. In zsh, \fBz\fR appends a function
 \fB_z_precmd\fR to the \fBprecmd_functions\fR array.
 \fB_z_precmd\fR to the \fBprecmd_functions\fR array.
 .P
 .P
@@ -139,8 +147,11 @@ The environment variable \fB$_Z_NO_PROMPT_COMMAND\fR can be set if you want to
 handle \fBPROMPT_COMMAND\fR or \fBprecmd\fR yourself.
 handle \fBPROMPT_COMMAND\fR or \fBprecmd\fR yourself.
 .P
 .P
 The environment variable \fB$_Z_EXCLUDE_DIRS\fR can be set to an array of
 The environment variable \fB$_Z_EXCLUDE_DIRS\fR can be set to an array of
-directories to exclude from tracking. \fB$HOME\fR is always excluded.
+directory trees to exclude from tracking. \fB$HOME\fR is always excluded.
 Directories must be full paths without trailing slashes.
 Directories must be full paths without trailing slashes.
+.P
+The environment variable \fB$_Z_OWNER\fR can be set to your username, to
+allow usage of \fBz\fR when your sudo enviroment keeps \fB$HOME\fR set.
 .SH
 .SH
 FILES
 FILES
 Data is stored in \fB$HOME/.z\fR. This can be overridden by setting the
 Data is stored in \fB$HOME/.z\fR. This can be overridden by setting the

+ 220 - 205
plugins/z/z.sh

@@ -3,29 +3,25 @@
 # maintains a jump-list of the directories you actually use
 # maintains a jump-list of the directories you actually use
 #
 #
 # INSTALL:
 # INSTALL:
-#   * put something like this in your .bashrc/.zshrc:
-#     . /path/to/z.sh
-#   * cd around for a while to build up the db
-#   * PROFIT!!
-#   * optionally:
-#     set $_Z_CMD in .bashrc/.zshrc to change the command (default z).
-#     set $_Z_DATA in .bashrc/.zshrc to change the datafile (default ~/.z).
-#     set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution.
-#     set $_Z_NO_PROMPT_COMMAND if you're handling PROMPT_COMMAND yourself.
-#     set $_Z_EXCLUDE_DIRS to an array of directories to exclude.
+#     * put something like this in your .bashrc/.zshrc:
+#         . /path/to/z.sh
+#     * cd around for a while to build up the db
+#     * PROFIT!!
+#     * optionally:
+#         set $_Z_CMD in .bashrc/.zshrc to change the command (default z).
+#         set $_Z_DATA in .bashrc/.zshrc to change the datafile (default ~/.z).
+#         set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution.
+#         set $_Z_NO_PROMPT_COMMAND if you're handling PROMPT_COMMAND yourself.
+#         set $_Z_EXCLUDE_DIRS to an array of directories to exclude.
+#         set $_Z_OWNER to your username if you want use z while sudo with $HOME kept
 #
 #
 # USE:
 # USE:
-#   * z foo     # cd to most frecent dir matching foo
-#   * z foo bar # cd to most frecent dir matching foo and bar
-#   * z -r foo  # cd to highest ranked dir matching foo
-#   * z -t foo  # cd to most recently accessed dir matching foo
-#   * z -l foo  # list matches instead of cd
-#   * z -c foo  # restrict matches to subdirs of $PWD
-
-case $- in
- *i*) ;;
-   *) echo 'ERROR: z.sh is meant to be sourced, not directly executed.'
-esac
+#     * z foo     # cd to most frecent dir matching foo
+#     * z foo bar # cd to most frecent dir matching foo and bar
+#     * z -r foo  # cd to highest ranked dir matching foo
+#     * z -t foo  # cd to most recently accessed dir matching foo
+#     * z -l foo  # list matches instead of cd
+#     * z -c foo  # restrict matches to subdirs of $PWD
 
 
 [ -d "${_Z_DATA:-$HOME/.z}" ] && {
 [ -d "${_Z_DATA:-$HOME/.z}" ] && {
     echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory."
     echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory."
@@ -33,196 +29,215 @@ esac
 
 
 _z() {
 _z() {
 
 
- local datafile="${_Z_DATA:-$HOME/.z}"
-
- # bail out if we don't own ~/.z (we're another user but our ENV is still set)
- [ -f "$datafile" -a ! -O "$datafile" ] && return
-
- # add entries
- if [ "$1" = "--add" ]; then
-  shift
-
-  # $HOME isn't worth matching
-  [ "$*" = "$HOME" ] && return
-
-  # don't track excluded dirs
-  local exclude
-  for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do
-   [ "$*" = "$exclude" ] && return
-  done
-
-  # maintain the file
-  local tempfile
-  tempfile="$(mktemp "$datafile.XXXXXX")" || return
-  while read line; do
-   [ -d "${line%%\|*}" ] && echo $line
-  done < "$datafile" | awk -v path="$*" -v now="$(date +%s)" -F"|" '
-   BEGIN {
-    rank[path] = 1
-    time[path] = now
-   }
-   $2 >= 1 {
-    if( $1 == path ) {
-     rank[$1] = $2 + 1
-     time[$1] = now
-    } else {
-     rank[$1] = $2
-     time[$1] = $3
-    }
-    count += $2
-   }
-   END {
-    if( count > 6000 ) {
-     for( i in rank ) print i "|" 0.99*rank[i] "|" time[i] # aging
-    } else for( i in rank ) print i "|" rank[i] "|" time[i]
-   }
-  ' 2>/dev/null >| "$tempfile"
-  if [ $? -ne 0 -a -f "$datafile" ]; then
-   env rm -f "$tempfile"
-  else
-   env mv -f "$tempfile" "$datafile"
-  fi
-
- # tab completion
- elif [ "$1" = "--complete" ]; then
-  while read line; do
-   [ -d "${line%%\|*}" ] && echo $line
-  done < "$datafile" | awk -v q="$2" -F"|" '
-   BEGIN {
-    if( q == tolower(q) ) nocase = 1
-    split(substr(q,3),fnd," ")
-   }
-   {
-    if( nocase ) {
-     for( i in fnd ) tolower($1) !~ tolower(fnd[i]) && $1 = ""
-    } else {
-     for( i in fnd ) $1 !~ fnd[i] && $1 = ""
-    }
-    if( $1 ) print $1
-   }
-  ' 2>/dev/null
-
- else
-  # list/go
-  while [ "$1" ]; do case "$1" in
-   --) while [ "$1" ]; do shift; local fnd="$fnd $1";done;;
-   -*) local opt=${1:1}; while [ "$opt" ]; do case ${opt:0:1} in
-        c) local fnd="^$PWD $fnd";;
-        h) echo "${_Z_CMD:-z} [-chlrt] args" >&2; return;;
-        l) local list=1;;
-        r) local typ="rank";;
-        t) local typ="recent";;
-       esac; opt=${opt:1}; done;;
-    *) local fnd="$fnd $1";;
-  esac; local last=$1; shift; done
-  [ "$fnd" -a "$fnd" != "^$PWD " ] || local list=1
-
-  # if we hit enter on a completion just go there
-  case "$last" in
-   # completions will always start with /
-   /*) [ -z "$list" -a -d "$last" ] && cd "$last" && return;;
-  esac
-
-  # no file yet
-  [ -f "$datafile" ] || return
-
-  local cd
-  cd="$(while read line; do
-   [ -d "${line%%\|*}" ] && echo $line
-  done < "$datafile" | awk -v t="$(date +%s)" -v list="$list" -v typ="$typ" -v q="$fnd" -F"|" '
-   function frecent(rank, time) {
-    dx = t-time
-    if( dx < 3600 ) return rank*4
-    if( dx < 86400 ) return rank*2
-    if( dx < 604800 ) return rank/2
-    return rank/4
-   }
-   function output(files, toopen, override) {
-    if( list ) {
-     cmd = "sort -n >&2"
-     for( i in files ) if( files[i] ) printf "%-10s %s\n", files[i], i | cmd
-     if( override ) printf "%-10s %s\n", "common:", override > "/dev/stderr"
-    } else {
-     if( override ) toopen = override
-     print toopen
-    }
-   }
-   function common(matches) {
-    # shortest match
-    for( i in matches ) {
-     if( matches[i] && (!short || length(i) < length(short)) ) short = i
-    }
-    if( short == "/" ) return
-    # shortest match must be common to each match. escape special characters in
-    # a copy when testing, so we can return the original.
-    clean_short = short
-    gsub(/[\(\)\[\]\|]/, "\\\\&", clean_short)
-    for( i in matches ) if( matches[i] && i !~ clean_short ) return
-    return short
-   }
-   BEGIN { split(q, a, " "); oldf = noldf = -9999999999 }
-   {
-    if( typ == "rank" ) {
-     f = $2
-    } else if( typ == "recent" ) {
-     f = $3-t
-    } else f = frecent($2, $3)
-    wcase[$1] = nocase[$1] = f
-    for( i in a ) {
-     if( $1 !~ a[i] ) delete wcase[$1]
-     if( tolower($1) !~ tolower(a[i]) ) delete nocase[$1]
-    }
-    if( wcase[$1] && wcase[$1] > oldf ) {
-     cx = $1
-     oldf = wcase[$1]
-    } else if( nocase[$1] && nocase[$1] > noldf ) {
-     ncx = $1
-     noldf = nocase[$1]
-    }
-   }
-   END {
-    if( cx ) {
-     output(wcase, cx, common(wcase))
-    } else if( ncx ) output(nocase, ncx, common(nocase))
-   }
-  ')"
-  [ $? -gt 0 ] && return
-  [ "$cd" ] && cd "$cd"
- fi
+    local datafile="${_Z_DATA:-$HOME/.z}"
+
+    # bail if we don't own ~/.z and $_Z_OWNER not set
+    [ -z "$_Z_OWNER" -a -f "$datafile" -a ! -O "$datafile" ] && return
+
+    # add entries
+    if [ "$1" = "--add" ]; then
+        shift
+
+        # $HOME isn't worth matching
+        [ "$*" = "$HOME" ] && return
+
+        # don't track excluded directory trees
+        local exclude
+        for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do
+            case "$*" in "$exclude*") return;; esac
+        done
+
+        # maintain the data file
+        local tempfile="$datafile.$RANDOM"
+        while read line; do
+            # only count directories
+            [ -d "${line%%\|*}" ] && echo $line
+        done < "$datafile" | awk -v path="$*" -v now="$(date +%s)" -F"|" '
+            BEGIN {
+                rank[path] = 1
+                time[path] = now
+            }
+            $2 >= 1 {
+                # drop ranks below 1
+                if( $1 == path ) {
+                    rank[$1] = $2 + 1
+                    time[$1] = now
+                } else {
+                    rank[$1] = $2
+                    time[$1] = $3
+                }
+                count += $2
+            }
+            END {
+                if( count > 9000 ) {
+                    # aging
+                    for( x in rank ) print x "|" 0.99*rank[x] "|" time[x]
+                } else for( x in rank ) print x "|" rank[x] "|" time[x]
+            }
+        ' 2>/dev/null >| "$tempfile"
+        # do our best to avoid clobbering the datafile in a race condition
+        if [ $? -ne 0 -a -f "$datafile" ]; then
+            env rm -f "$tempfile"
+        else
+            [ "$_Z_OWNER" ] && chown $_Z_OWNER:$(id -ng $_Z_OWNER) "$tempfile"
+            env mv -f "$tempfile" "$datafile" || env rm -f "$tempfile"
+        fi
+
+    # tab completion
+    elif [ "$1" = "--complete" -a -s "$datafile" ]; then
+        while read line; do
+            [ -d "${line%%\|*}" ] && echo $line
+        done < "$datafile" | awk -v q="$2" -F"|" '
+            BEGIN {
+                if( q == tolower(q) ) imatch = 1
+                q = substr(q, 3)
+                gsub(" ", ".*", q)
+            }
+            {
+                if( imatch ) {
+                    if( tolower($1) ~ tolower(q) ) print $1
+                } else if( $1 ~ q ) print $1
+            }
+        ' 2>/dev/null
+
+    else
+        # list/go
+        while [ "$1" ]; do case "$1" in
+            --) while [ "$1" ]; do shift; local fnd="$fnd${fnd:+ }$1";done;;
+            -*) local opt=${1:1}; while [ "$opt" ]; do case ${opt:0:1} in
+                    c) local fnd="^$PWD $fnd";;
+                    h) echo "${_Z_CMD:-z} [-chlrtx] args" >&2; return;;
+                    x) sed -i -e "\:^${PWD}|.*:d" "$datafile";;
+                    l) local list=1;;
+                    r) local typ="rank";;
+                    t) local typ="recent";;
+                esac; opt=${opt:1}; done;;
+             *) local fnd="$fnd${fnd:+ }$1";;
+        esac; local last=$1; [ "$#" -gt 0 ] && shift; done
+        [ "$fnd" -a "$fnd" != "^$PWD " ] || local list=1
+
+        # if we hit enter on a completion just go there
+        case "$last" in
+            # completions will always start with /
+            /*) [ -z "$list" -a -d "$last" ] && cd "$last" && return;;
+        esac
+
+        # no file yet
+        [ -f "$datafile" ] || return
+
+        local cd
+        cd="$(while read line; do
+            [ -d "${line%%\|*}" ] && echo $line
+        done < "$datafile" | awk -v t="$(date +%s)" -v list="$list" -v typ="$typ" -v q="$fnd" -F"|" '
+            function frecent(rank, time) {
+                # relate frequency and time
+                dx = t - time
+                if( dx < 3600 ) return rank * 4
+                if( dx < 86400 ) return rank * 2
+                if( dx < 604800 ) return rank / 2
+                return rank / 4
+            }
+            function output(files, out, common) {
+                # list or return the desired directory
+                if( list ) {
+                    cmd = "sort -n >&2"
+                    for( x in files ) {
+                        if( files[x] ) printf "%-10s %s\n", files[x], x | cmd
+                    }
+                    if( common ) {
+                        printf "%-10s %s\n", "common:", common > "/dev/stderr"
+                    }
+                } else {
+                    if( common ) out = common
+                    print out
+                }
+            }
+            function common(matches) {
+                # find the common root of a list of matches, if it exists
+                for( x in matches ) {
+                    if( matches[x] && (!short || length(x) < length(short)) ) {
+                        short = x
+                    }
+                }
+                if( short == "/" ) return
+                # use a copy to escape special characters, as we want to return
+                # the original. yeah, this escaping is awful.
+                clean_short = short
+                gsub(/\[\(\)\[\]\|\]/, "\\\\&", clean_short)
+                for( x in matches ) if( matches[x] && x !~ clean_short ) return
+                return short
+            }
+            BEGIN {
+                gsub(" ", ".*", q)
+                hi_rank = ihi_rank = -9999999999
+            }
+            {
+                if( typ == "rank" ) {
+                    rank = $2
+                } else if( typ == "recent" ) {
+                    rank = $3 - t
+                } else rank = frecent($2, $3)
+                if( $1 ~ q ) {
+                    matches[$1] = rank
+                } else if( tolower($1) ~ tolower(q) ) imatches[$1] = rank
+                if( matches[$1] && matches[$1] > hi_rank ) {
+                    best_match = $1
+                    hi_rank = matches[$1]
+                } else if( imatches[$1] && imatches[$1] > ihi_rank ) {
+                    ibest_match = $1
+                    ihi_rank = imatches[$1]
+                }
+            }
+            END {
+                # prefer case sensitive
+                if( best_match ) {
+                    output(matches, best_match, common(matches))
+                } else if( ibest_match ) {
+                    output(imatches, ibest_match, common(imatches))
+                }
+            }
+        ')"
+        [ $? -gt 0 ] && return
+        [ "$cd" ] && cd "$cd"
+    fi
 }
 }
 
 
 alias ${_Z_CMD:-z}='_z 2>&1'
 alias ${_Z_CMD:-z}='_z 2>&1'
 
 
 [ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P"
 [ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P"
 
 
-if compctl &> /dev/null; then
- [ "$_Z_NO_PROMPT_COMMAND" ] || {
-  # zsh populate directory list, avoid clobbering any other precmds
-  if [ "$_Z_NO_RESOLVE_SYMLINKS" ]; then
-    _z_precmd() {
-      _z --add "${PWD:a}"
+if type compctl >/dev/null 2>&1; then
+    # zsh
+    [ "$_Z_NO_PROMPT_COMMAND" ] || {
+        # populate directory list, avoid clobbering any other precmds.
+        if [ "$_Z_NO_RESOLVE_SYMLINKS" ]; then
+            _z_precmd() {
+                _z --add "${PWD:a}"
+            }
+        else
+            _z_precmd() {
+                _z --add "${PWD:A}"
+            }
+        fi
+        [[ -n "${precmd_functions[(r)_z_precmd]}" ]] || {
+            precmd_functions[$(($#precmd_functions+1))]=_z_precmd
+        }
+    }
+    _z_zsh_tab_completion() {
+        # tab completion
+        local compl
+        read -l compl
+        reply=(${(f)"$(_z --complete "$compl")"})
     }
     }
-  else
-    _z_precmd() {
-      _z --add "${PWD:A}"
+    compctl -U -K _z_zsh_tab_completion _z
+elif type complete >/dev/null 2>&1; then
+    # bash
+    # tab completion
+    complete -o filenames -C '_z --complete "$COMP_LINE"' ${_Z_CMD:-z}
+    [ "$_Z_NO_PROMPT_COMMAND" ] || {
+        # populate directory list. avoid clobbering other PROMPT_COMMANDs.
+        grep "_z --add" <<< "$PROMPT_COMMAND" >/dev/null || {
+            PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null;'
+        }
     }
     }
-  fi
-  precmd_functions+=(_z_precmd)
- }
- # zsh tab completion
- _z_zsh_tab_completion() {
-  local compl
-  read -l compl
-  reply=(${(f)"$(_z --complete "$compl")"})
- }
- compctl -U -K _z_zsh_tab_completion _z
-elif complete &> /dev/null; then
- # bash tab completion
- complete -o filenames -C '_z --complete "$COMP_LINE"' ${_Z_CMD:-z}
- [ "$_Z_NO_PROMPT_COMMAND" ] || {
-  # bash populate directory list. avoid clobbering other PROMPT_COMMANDs.
-  echo $PROMPT_COMMAND | grep -q "_z --add" || {
-   PROMPT_COMMAND='_z --add "$(pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null;'"$PROMPT_COMMAND"
-  }
- }
 fi
 fi