z.sh 8.69 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
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
17
18
#
# USE:
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
19
20
21
22
23
#     * 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
24
#     * z -e foo  # echo the best match, don't cd
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
25
#     * z -c foo  # restrict matches to subdirs of $PWD
26
27
28
29
30
31
32

[ -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
33
34
    local datafile="${_Z_DATA:-$HOME/.z}"

35
36
37
    # if symlink, dereference
    [ -h "$datafile" ] && datafile=$(readlink "$datafile")

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

41
42
43
44
45
46
47
48
49
    _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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
    # 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"
65
        _z_dirs | awk -v path="$*" -v now="$(date +%s)" -F"|" '
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
            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"
88
        # do our best to avoid clobbering the datafile in a race condition.
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
89
90
91
92
93
94
95
96
97
        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
98
        _z_dirs | awk -v q="$2" -F"|" '
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
99
100
            BEGIN {
                q = substr(q, 3)
101
102
                if( q == tolower(q) ) imatch = 1
                gsub(/ /, ".*", q)
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
103
104
105
            }
            {
                if( imatch ) {
106
                    if( tolower($1) ~ q ) print $1
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
107
108
109
110
111
112
113
114
115
116
                } 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";;
117
118
                    e) local echo=1;;
                    h) echo "${_Z_CMD:-z} [-cehlrtx] args" >&2; return;;
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
119
120
121
                    l) local list=1;;
                    r) local typ="rank";;
                    t) local typ="recent";;
122
                    x) sed -i -e "\:^${PWD}|.*:d" "$datafile";;
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
123
124
125
126
127
128
129
130
                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 /
131
            /*) [ -z "$list" -a -d "$last" ] && builtin cd "$last" && return;;
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
132
133
134
135
136
137
        esac

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

        local cd
138
        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
139
140
141
142
143
144
145
146
            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
            }
147
            function output(matches, best_match, common) {
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
148
149
150
                # list or return the desired directory
                if( list ) {
                    cmd = "sort -n >&2"
151
152
153
154
                    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
155
156
157
158
159
                    }
                    if( common ) {
                        printf "%-10s %s\n", "common:", common > "/dev/stderr"
                    }
                } else {
160
161
                    if( common ) best_match = common
                    print best_match
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
162
163
164
165
166
167
168
169
170
171
                }
            }
            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
172
173
174
                for( x in matches ) if( matches[x] && index(x, short) != 1 ) {
                    return
                }
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
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
204
205
206
                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))
                }
            }
        ')"
207
208
209
210

        [ $? -eq 0 ] && [ "$cd" ] && {
          if [ "$echo" ]; then echo "$cd"; else builtin cd "$cd"; fi
        }
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
211
    fi
212
213
214
215
216
217
}

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
218
219
220
221
222
223
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() {
224
                (_z --add "${PWD:a}" &)
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
225
226
227
            }
        else
            _z_precmd() {
228
                (_z --add "${PWD:A}" &)
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
229
230
231
232
233
234
235
236
237
238
239
            }
        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")"})
240
    }
Eduardo San Martin Morote's avatar
Eduardo San Martin Morote committed
241
242
243
244
245
246
247
248
    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 || {
249
            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
250
        }
251
252
    }
fi