z.sh 9.01 KB
Newer Older
1
# Copyright (c) 2009 rupa deadwyler. Licensed under the WTFPL license, Version 2
2
3
4
5

# maintains a jump-list of the directories you actually use
#
# INSTALL:
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
6
7
8
9
10
11
12
#     * 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).
13
#         set $_Z_MAX_SCORE lower to age entries out faster (default 9000).
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
14
15
16
17
#         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
18
19
#
# USE:
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
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
25
#     * z -e foo  # echo the best match, don't cd
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
26
#     * z -c foo  # restrict matches to subdirs of $PWD
27
28
#     * z -x      # remove the current directory from the datafile
#     * z -h      # show a brief help message
29
30
31
32
33
34
35

[ -d "${_Z_DATA:-$HOME/.z}" ] && {
    echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory."
}

_z() {

Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
36
37
    local datafile="${_Z_DATA:-$HOME/.z}"

38
39
40
    # if symlink, dereference
    [ -h "$datafile" ] && datafile=$(readlink "$datafile")

Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
41
42
43
    # bail if we don't own ~/.z and $_Z_OWNER not set
    [ -z "$_Z_OWNER" -a -f "$datafile" -a ! -O "$datafile" ] && return

44
45
46
47
48
49
50
51
52
    _z_dirs () {
        local line
        while read line; do
            # only count directories
            [ -d "${line%%\|*}" ] && echo "$line"
        done < "$datafile"
        return 0
    }

Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
    # 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"
68
69
        local score=${_Z_MAX_SCORE:-9000}
        _z_dirs | awk -v path="$*" -v now="$(date +%s)" -v score=$score -F"|" '
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
            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 {
86
                if( count > score ) {
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
87
88
89
90
91
                    # 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"
92
        # do our best to avoid clobbering the datafile in a race condition.
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
93
94
95
        if [ $? -ne 0 -a -f "$datafile" ]; then
            env rm -f "$tempfile"
        else
96
            [ "$_Z_OWNER" ] && chown $_Z_OWNER:"$(id -ng $_Z_OWNER)" "$tempfile"
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
97
98
99
100
101
            env mv -f "$tempfile" "$datafile" || env rm -f "$tempfile"
        fi

    # tab completion
    elif [ "$1" = "--complete" -a -s "$datafile" ]; then
102
        _z_dirs | awk -v q="$2" -F"|" '
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
103
104
            BEGIN {
                q = substr(q, 3)
105
106
                if( q == tolower(q) ) imatch = 1
                gsub(/ /, ".*", q)
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
107
108
109
            }
            {
                if( imatch ) {
110
                    if( tolower($1) ~ q ) print $1
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
111
112
113
114
115
116
                } else if( $1 ~ q ) print $1
            }
        ' 2>/dev/null

    else
        # list/go
117
        local echo fnd last list opt typ
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
118
        while [ "$1" ]; do case "$1" in
119
120
121
122
            --) while [ "$1" ]; do shift; fnd="$fnd${fnd:+ }$1";done;;
            -*) opt=${1:1}; while [ "$opt" ]; do case ${opt:0:1} in
                    c) fnd="^$PWD $fnd";;
                    e) echo=1;;
123
                    h) echo "${_Z_CMD:-z} [-cehlrtx] args" >&2; return;;
124
125
126
                    l) list=1;;
                    r) typ="rank";;
                    t) typ="recent";;
127
                    x) sed -i -e "\:^${PWD}|.*:d" "$datafile";;
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
128
                esac; opt=${opt:1}; done;;
129
130
131
             *) fnd="$fnd${fnd:+ }$1";;
        esac; last=$1; [ "$#" -gt 0 ] && shift; done
        [ "$fnd" -a "$fnd" != "^$PWD " ] || list=1
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
132
133
134
135

        # if we hit enter on a completion just go there
        case "$last" in
            # completions will always start with /
136
            /*) [ -z "$list" -a -d "$last" ] && builtin cd "$last" && return;;
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
137
138
139
140
141
142
        esac

        # no file yet
        [ -f "$datafile" ] || return

        local cd
143
        cd="$( < <( _z_dirs ) awk -v t="$(date +%s)" -v list="$list" -v typ="$typ" -v q="$fnd" -F"|" '
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
144
            function frecent(rank, time) {
145
146
147
              # relate frequency and time
              dx = t - time
              return int(10000 * rank * (3.75/((0.0001 * dx + 1) + 0.25)))
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
148
            }
149
            function output(matches, best_match, common) {
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
150
151
                # list or return the desired directory
                if( list ) {
152
153
154
155
                    if( common ) {
                        printf "%-10s %s\n", "common:", common > "/dev/stderr"
                    }
                    cmd = "sort -n >&2"
156
157
158
159
                    for( x in matches ) {
                        if( matches[x] ) {
                            printf "%-10s %s\n", matches[x], x | cmd
                        }
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
160
161
                    }
                } else {
162
                    if( common && !typ ) best_match = common
163
                    print best_match
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
164
165
166
167
168
169
170
171
172
173
                }
            }
            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
174
175
176
                for( x in matches ) if( matches[x] && index(x, short) != 1 ) {
                    return
                }
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
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
                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))
204
                    exit
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
205
206
                } else if( ibest_match ) {
                    output(imatches, ibest_match, common(imatches))
207
                    exit
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
208
                }
209
                exit(1)
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
210
211
            }
        ')"
212

213
214
215
216
217
218
219
        if [ "$?" -eq 0 ]; then
          if [ "$cd" ]; then
            if [ "$echo" ]; then echo "$cd"; else builtin cd "$cd"; fi
          fi
        else
          return $?
        fi
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
220
    fi
221
222
223
224
225
226
}

alias ${_Z_CMD:-z}='_z 2>&1'

[ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P"

Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
227
228
229
230
231
232
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() {
233
                (_z --add "${PWD:a}" &)
234
                : $RANDOM
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
235
236
237
            }
        else
            _z_precmd() {
238
                (_z --add "${PWD:A}" &)
239
                : $RANDOM
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
240
241
242
243
244
245
246
247
248
249
250
            }
        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")"})
251
    }
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
252
253
254
255
256
257
258
259
    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 || {
260
            PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''(_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null &);'
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
261
        }
262
263
    }
fi