all repos — dotfiles @ 037cb472cc122197838a1c583d6202e556c23dff

my *nix dotfiles

home/bin/z.sh (view raw)

  1#!/usr/bin/env bash
  2[ -d "${_Z_DATA:-$HOME/.z}" ] && {
  3    echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory."
  4}
  5
  6_z() {
  7
  8    local datafile="${_Z_DATA:-$HOME/.z}"
  9
 10    # if symlink, dereference
 11    [ -h "$datafile" ] && datafile=$(readlink "$datafile")
 12
 13    # bail if we don't own ~/.z and $_Z_OWNER not set
 14    [ -z "$_Z_OWNER" -a -f "$datafile" -a ! -O "$datafile" ] && return
 15
 16    _z_dirs () {
 17        [ -f "$datafile" ] || return
 18
 19        local line
 20        while read line; do
 21            # only count directories
 22            [ -d "${line%%\|*}" ] && echo "$line"
 23        done < "$datafile"
 24        return 0
 25    }
 26
 27    # add entries
 28    if [ "$1" = "--add" ]; then
 29        shift
 30
 31        # $HOME and / aren't worth matching
 32        [ "$*" = "$HOME" -o "$*" = '/' ] && return
 33
 34        # don't track excluded directory trees
 35        if [ ${#_Z_EXCLUDE_DIRS[@]} -gt 0 ]; then
 36            local exclude
 37            for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do
 38                case "$*" in "$exclude"*) return;; esac
 39            done
 40        fi
 41
 42        # maintain the data file
 43        local tempfile="$datafile.$RANDOM"
 44        local score=${_Z_MAX_SCORE:-9000}
 45        _z_dirs | awk -v path="$*" -v now="$(date +%s)" -v score=$score -F"|" '
 46            BEGIN {
 47                rank[path] = 1
 48                time[path] = now
 49            }
 50            $2 >= 1 {
 51                # drop ranks below 1
 52                if( $1 == path ) {
 53                    rank[$1] = $2 + 1
 54                    time[$1] = now
 55                } else {
 56                    rank[$1] = $2
 57                    time[$1] = $3
 58                }
 59                count += $2
 60            }
 61            END {
 62                if( count > score ) {
 63                    # aging
 64                    for( x in rank ) print x "|" 0.99*rank[x] "|" time[x]
 65                } else for( x in rank ) print x "|" rank[x] "|" time[x]
 66            }
 67        ' 2>/dev/null >| "$tempfile"
 68        # do our best to avoid clobbering the datafile in a race condition.
 69        if [ $? -ne 0 -a -f "$datafile" ]; then
 70            env rm -f "$tempfile"
 71        else
 72            [ "$_Z_OWNER" ] && chown $_Z_OWNER:"$(id -ng $_Z_OWNER)" "$tempfile"
 73            env mv -f "$tempfile" "$datafile" || env rm -f "$tempfile"
 74        fi
 75
 76    # tab completion
 77    elif [ "$1" = "--complete" -a -s "$datafile" ]; then
 78        _z_dirs | awk -v q="$2" -F"|" '
 79            BEGIN {
 80                q = substr(q, 3)
 81                if( q == tolower(q) ) imatch = 1
 82                gsub(/ /, ".*", q)
 83            }
 84            {
 85                if( imatch ) {
 86                    if( tolower($1) ~ q ) print $1
 87                } else if( $1 ~ q ) print $1
 88            }
 89        ' 2>/dev/null
 90
 91    else
 92        # list/go
 93        local echo fnd last list opt typ
 94        while [ "$1" ]; do case "$1" in
 95            --) while [ "$1" ]; do shift; fnd="$fnd${fnd:+ }$1";done;;
 96            -*) opt=${1:1}; while [ "$opt" ]; do case ${opt:0:1} in
 97                    c) fnd="^$PWD $fnd";;
 98                    e) echo=1;;
 99                    h) echo "${_Z_CMD:-z} [-cehlrtx] args" >&2; return;;
100                    l) list=1;;
101                    r) typ="rank";;
102                    t) typ="recent";;
103                    x) sed -i -e "\:^${PWD}|.*:d" "$datafile";;
104                esac; opt=${opt:1}; done;;
105             *) fnd="$fnd${fnd:+ }$1";;
106        esac; last=$1; [ "$#" -gt 0 ] && shift; done
107        [ "$fnd" -a "$fnd" != "^$PWD " ] || list=1
108
109        # if we hit enter on a completion just go there
110        case "$last" in
111            # completions will always start with /
112            /*) [ -z "$list" -a -d "$last" ] && builtin cd "$last" && return;;
113        esac
114
115        # no file yet
116        [ -f "$datafile" ] || return
117
118        local cd
119        cd="$( < <( _z_dirs ) awk -v t="$(date +%s)" -v list="$list" -v typ="$typ" -v q="$fnd" -F"|" '
120            function frecent(rank, time) {
121              # relate frequency and time
122              dx = t - time
123              return int(10000 * rank * (3.75/((0.0001 * dx + 1) + 0.25)))
124            }
125            function output(matches, best_match, common) {
126                # list or return the desired directory
127                if( list ) {
128                    if( common ) {
129                        printf "%-10s %s\n", "common:", common > "/dev/stderr"
130                    }
131                    cmd = "sort -n >&2"
132                    for( x in matches ) {
133                        if( matches[x] ) {
134                            printf "%-10s %s\n", matches[x], x | cmd
135                        }
136                    }
137                } else {
138                    if( common && !typ ) best_match = common
139                    print best_match
140                }
141            }
142            function common(matches) {
143                # find the common root of a list of matches, if it exists
144                for( x in matches ) {
145                    if( matches[x] && (!short || length(x) < length(short)) ) {
146                        short = x
147                    }
148                }
149                if( short == "/" ) return
150                for( x in matches ) if( matches[x] && index(x, short) != 1 ) {
151                    return
152                }
153                return short
154            }
155            BEGIN {
156                gsub(" ", ".*", q)
157                hi_rank = ihi_rank = -9999999999
158            }
159            {
160                if( typ == "rank" ) {
161                    rank = $2
162                } else if( typ == "recent" ) {
163                    rank = $3 - t
164                } else rank = frecent($2, $3)
165                if( $1 ~ q ) {
166                    matches[$1] = rank
167                } else if( tolower($1) ~ tolower(q) ) imatches[$1] = rank
168                if( matches[$1] && matches[$1] > hi_rank ) {
169                    best_match = $1
170                    hi_rank = matches[$1]
171                } else if( imatches[$1] && imatches[$1] > ihi_rank ) {
172                    ibest_match = $1
173                    ihi_rank = imatches[$1]
174                }
175            }
176            END {
177                # prefer case sensitive
178                if( best_match ) {
179                    output(matches, best_match, common(matches))
180                    exit
181                } else if( ibest_match ) {
182                    output(imatches, ibest_match, common(imatches))
183                    exit
184                }
185                exit(1)
186            }
187        ')"
188
189        if [ "$?" -eq 0 ]; then
190          if [ "$cd" ]; then
191            if [ "$echo" ]; then echo "$cd"; else builtin cd "$cd"; fi
192          fi
193        else
194          return $?
195        fi
196    fi
197}
198
199alias ${_Z_CMD:-z}='_z 2>&1'
200
201[ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P"
202
203if type complete >/dev/null 2>&1; then
204    # bash
205    # tab completion
206    complete -o filenames -C '_z --complete "$COMP_LINE"' ${_Z_CMD:-z}
207    [ "$_Z_NO_PROMPT_COMMAND" ] || {
208        # populate directory list. avoid clobbering other PROMPT_COMMANDs.
209        grep "_z --add" <<< "$PROMPT_COMMAND" >/dev/null || {
210            PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''(_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null &);'
211        }
212    }
213fi