Files
z/z.sh
T

204 lines
7.4 KiB
Bash
Raw Normal View History

# Copyright (c) 2009 rupa deadwyler under the WTFPL license
2009-06-18 22:15:33 -07:00
2009-06-26 11:22:25 -04:00
# maintains a jump-list of the directories you actually use
#
# INSTALL:
2013-06-18 20:38:28 -04:00
# * 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.
2009-06-26 11:22:25 -04:00
#
# USE:
2013-06-18 20:38:28 -04:00
# * 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
2009-06-18 22:15:33 -07:00
[ -d "${_Z_DATA:-$HOME/.z}" ] && {
echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory."
}
_z() {
2013-06-18 20:38:28 -04:00
local datafile="${_Z_DATA:-$HOME/.z}"
2013-06-18 20:38:28 -04:00
# bail if we don't own ~/.z (we're another user but our ENV is still set)
[ -f "$datafile" -a ! -O "$datafile" ] && return
2014-03-26 13:02:21 -04:00
local exclude;
2013-06-18 20:38:28 -04:00
# tab completion
2013-12-24 19:20:47 -05:00
if [ "$1" = "--complete" ]; then
2013-06-18 20:38:28 -04:00
while read line; do
[ -d "${line%%\|*}" ] && echo $line
2014-03-26 13:02:21 -04:00
for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do
[ "${line%%\|*}" = "$exclude" ] && continue
done
2013-06-18 20:38:28 -04:00
done < "$datafile" | awk -v q="$2" -F"|" '
BEGIN {
2013-06-18 21:37:19 -04:00
if( q == tolower(q) ) imatch = 1
2013-08-18 02:26:08 -04:00
q = substr(q, 3)
gsub(" ", ".*", q)
2013-06-18 20:38:28 -04:00
}
{
2013-06-18 21:37:19 -04:00
if( imatch ) {
2013-08-18 02:26:08 -04:00
if( tolower($1) ~ tolower(q) ) print $1
} else if( $1 ~ q ) print $1
2013-06-18 20:38:28 -04:00
}
' 2>/dev/null
2013-06-18 20:38:28 -04:00
else
# list/go
while [ "$1" ]; do case "$1" in
2013-08-18 14:12:51 -04:00
--) while [ "$1" ]; do shift; local fnd="$fnd${fnd:+ }$1";done;;
2013-06-18 20:38:28 -04:00
-*) 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";;
2013-06-18 20:38:28 -04:00
l) local list=1;;
r) local typ="rank";;
t) local typ="recent";;
esac; opt=${opt:1}; done;;
2013-08-18 14:12:51 -04:00
*) local fnd="$fnd${fnd:+ }$1";;
2013-06-18 20:38:28 -04:00
esac; local last=$1; shift; done
[ "$fnd" -a "$fnd" != "^$PWD " ] || local list=1
2013-06-18 20:38:28 -04:00
# 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
2013-06-18 20:38:28 -04:00
# no file yet
[ -f "$datafile" ] || return
2013-06-18 20:38:28 -04:00
local cd
cd="$(while read line; do
[ -d "${line%%\|*}" ] && echo $line
2014-03-26 13:02:21 -04:00
for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do
[ "${line%%\|*}" = "$exclude" ] && continue
done
2013-06-18 20:38:28 -04:00
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
2013-06-18 21:37:19 -04:00
dx = t - time
if( dx < 3600 ) return rank * 4
if( dx < 86400 ) return rank * 2
if( dx < 604800 ) return rank / 2
return rank / 4
2013-06-18 20:38:28 -04:00
}
2013-06-18 21:37:19 -04:00
function output(files, out, common) {
2013-06-18 20:38:28 -04:00
# list or return the desired directory
if( list ) {
cmd = "sort -n >&2"
2013-06-18 21:37:19 -04:00
for( x in files ) {
if( files[x] ) printf "%-10s %s\n", files[x], x | cmd
2013-06-18 20:38:28 -04:00
}
if( common ) {
printf "%-10s %s\n", "common:", common > "/dev/stderr"
}
} else {
2013-06-18 21:37:19 -04:00
if( common ) out = common
print out
2013-06-18 20:38:28 -04:00
}
}
function common(matches) {
2013-06-18 21:37:19 -04:00
# 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
2013-06-18 20:38:28 -04:00
}
}
if( short == "/" ) return
# use a copy to escape special characters, as we want to return
2013-06-18 21:37:19 -04:00
# the original. yeah, this escaping is awful.
2013-06-18 20:38:28 -04:00
clean_short = short
gsub(/[\(\)\[\]\|]/, "\\\\&", clean_short)
2013-06-18 21:37:19 -04:00
for( x in matches ) if( matches[x] && x !~ clean_short ) return
2013-06-18 20:38:28 -04:00
return short
}
2013-08-18 02:26:08 -04:00
BEGIN {
gsub(" ", ".*", q)
hi_rank = ihi_rank = -9999999999
}
2013-06-18 20:38:28 -04:00
{
if( typ == "rank" ) {
2013-06-18 21:37:19 -04:00
rank = $2
2013-06-18 20:38:28 -04:00
} else if( typ == "recent" ) {
2013-06-18 21:37:19 -04:00
rank = $3 - t
} else rank = frecent($2, $3)
2013-08-18 02:26:08 -04:00
if( $1 ~ q ) {
matches[$1] = rank
} else if( tolower($1) ~ tolower(q) ) imatches[$1] = rank
2013-06-18 21:37:19 -04:00
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]
2013-06-18 20:38:28 -04:00
}
}
END {
2013-06-18 21:37:19 -04:00
# prefer case sensitive
if( best_match ) {
output(matches, best_match, common(matches))
} else if( ibest_match ) {
output(imatches, ibest_match, common(imatches))
}
2013-06-18 20:38:28 -04:00
}
')"
[ $? -gt 0 ] && return
[ "$cd" ] && cd "$cd"
fi
2009-06-18 22:15:33 -07:00
}
alias ${_Z_CMD:-z}='_z 2>&1'
2013-12-24 23:55:19 -05:00
[ "$_Z_NO_RESOLVE_SYMLINKS" ] || _Z_RESOLVE_SYMLINKS="-P"
2013-07-07 17:48:07 -04:00
if compctl >/dev/null 2>&1; then
2013-06-18 20:38:28 -04:00
# zsh
[ "$_Z_NO_PROMPT_COMMAND" ] || {
2013-06-20 00:43:14 -04:00
# populate directory list, avoid clobbering any other precmds.
2013-12-24 23:55:19 -05:00
if [ "$_Z_NO_RESOLVE_SYMLINKS" ]; then
_z_precmd() {
/usr/local/bin/_z_prompt_command "${PWD:a}"
}
else
_z_precmd() {
/usr/local/bin/_z_prompt_command "${PWD:A}"
}
fi
2013-07-30 10:23:34 -04:00
[[ -n "${precmd_functions[(r)_z_precmd]}" ]] || {
2013-12-12 20:01:41 -05:00
precmd_functions[$(($#precmd_functions+1))]=_z_precmd
2013-07-30 10:23:34 -04:00
}
}
2013-06-18 20:38:28 -04:00
_z_zsh_tab_completion() {
# tab completion
local compl
read -l compl
reply=(${(f)"$(_z --complete "$compl")"})
}
2013-06-18 20:38:28 -04:00
compctl -U -K _z_zsh_tab_completion _z
2013-07-07 17:48:07 -04:00
elif complete >/dev/null 2>&1; then
2013-06-18 20:38:28 -04:00
# bash
# tab completion
complete -o filenames -C '_z --complete "$COMP_LINE"' ${_Z_CMD:-z}
[ "$_Z_NO_PROMPT_COMMAND" ] || {
2013-06-20 00:43:14 -04:00
# populate directory list. avoid clobbering other PROMPT_COMMANDs.
2013-12-24 19:20:47 -05:00
grep "_z_prompt_command" <<< "$PROMPT_COMMAND" >/dev/null || {
2013-12-24 23:55:19 -05:00
PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''/usr/local/bin/_z_prompt_command "$(pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)";'
2013-06-18 20:38:28 -04:00
}
}
fi
2013-02-24 16:20:17 -05:00
# we can unset these as they are only used during setup
unset -v _Z_NO_PROMPT_COMMAND _Z_NO_RESOLVE_SYMLINKS _Z_RESOLVE_SYMLINKS