Browse Source

Updated z version

Closes #3248
Closes #4570
Eduardo San Martin Morote 9 years ago
parent
commit
d3363964a2
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
 
 SYNOPSIS
-       z [-chlrt] [regex1 regex2 ... regexn]
+       z [-chlrtx] [regex1 regex2 ... regexn]
 
 AVAILABILITY
        bash, zsh
@@ -15,10 +15,13 @@ DESCRIPTION
        Tracks your most used directories, based on 'frecency'.
 
        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
-       -c     restrict matches to subdirectories of the current directory.
+       -c     restrict matches to subdirectories of the current directory
 
        -h     show a brief help message
 
@@ -28,10 +31,12 @@ OPTIONS
 
        -t     match by recent access only
 
+       -x     remove the current directory from the datafile
+
 EXAMPLES
        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
 
@@ -55,8 +60,9 @@ NOTES
               Set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution.
               Set $_Z_NO_PROMPT_COMMAND to handle PROMPT_COMMAND/precmd  your-
               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.)
               Install   the   provided   man   page   z.1    somewhere    like
               /usr/local/man/man1.
@@ -64,12 +70,12 @@ NOTES
    Aging:
        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
-       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 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.
 
        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
        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
        to handle PROMPT_COMMAND or precmd yourself.
 
        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
-       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.
 
        A man page (z.1) is provided.

+ 23 - 12
plugins/z/z.1

@@ -4,7 +4,7 @@ NAME
 z \- jump around
 .SH
 SYNOPSIS
-z [\-chlrt] [regex1 regex2 ... regexn]
+z [\-chlrtx] [regex1 regex2 ... regexn]
 .SH
 AVAILABILITY
 bash, zsh
@@ -13,12 +13,14 @@ DESCRIPTION
 Tracks your most used directories, based on 'frecency'.
 .P
 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
 OPTIONS
 .TP
 \fB\-c\fR
-restrict matches to subdirectories of the current directory.
+restrict matches to subdirectories of the current directory
 .TP
 \fB\-h\fR
 show a brief help message
@@ -31,13 +33,16 @@ match by rank only
 .TP
 \fB\-t\fR
 match by recent access only
+.TP
+\fB\-x\fR
+remove the current directory from the datafile
 .SH EXAMPLES
 .TP 14
 \fBz foo\fR
 cd to most frecent dir matching foo
 .TP 14
 \fBz foo bar\fR
-cd to most frecent dir matching foo and bar
+cd to most frecent dir matching foo, then bar
 .TP 14
 \fBz -r foo\fR
 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.
 .RE
 .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
 .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
 .RS
 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:
 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
-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
 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.
 .P
 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
 added to the datafile.
 .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
 \fB_z_precmd\fR to the \fBprecmd_functions\fR array.
 .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.
 .P
 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.
+.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
 FILES
 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
 #
 # 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:
-#   * 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}" ] && {
     echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory."
@@ -33,196 +29,215 @@ esac
 
 _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'
 
 [ "$_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