Blame view

plugins/z/z.sh 8.62 KB
093a6c34b   mj   Squashed 'repos/r...
1
2
3
4
5
  # Copyright (c) 2009 rupa deadwyler under the WTFPL license
  
  # maintains a jump-list of the directories you actually use
  #
  # INSTALL:
7378b55de   mj   Squashed 'repos/r...
6
7
8
9
10
11
12
13
14
15
16
  #     * 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
093a6c34b   mj   Squashed 'repos/r...
17
18
  #
  # USE:
7378b55de   mj   Squashed 'repos/r...
19
20
21
22
23
24
  #     * 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
093a6c34b   mj   Squashed 'repos/r...
25
26
27
28
29
30
  
  [ -d "${_Z_DATA:-$HOME/.z}" ] && {
      echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory."
  }
  
  _z() {
7378b55de   mj   Squashed 'repos/r...
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
      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
  ", files[x], x | cmd
                      }
                      if( common ) {
                          printf "%-10s %s
  ", "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
093a6c34b   mj   Squashed 'repos/r...
204
205
206
207
208
  }
  
  alias ${_Z_CMD:-z}='_z 2>&1'
  
  [ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P"
7378b55de   mj   Squashed 'repos/r...
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
  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")"})
093a6c34b   mj   Squashed 'repos/r...
231
      }
7378b55de   mj   Squashed 'repos/r...
232
233
234
235
236
237
238
239
240
241
242
      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"$'
  ''_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null;'
          }
093a6c34b   mj   Squashed 'repos/r...
243
      }
093a6c34b   mj   Squashed 'repos/r...
244
  fi