diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a1b7448 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README b/README index 7de82a4..47e54c5 100644 --- a/README +++ b/README @@ -23,6 +23,8 @@ DESCRIPTION OPTIONS -c restrict matches to subdirectories of the current directory + -e echo the best match, don't cd + -h show a brief help message -l list only @@ -57,6 +59,8 @@ NOTES Optionally: Set $_Z_CMD to change the command name (default z). Set $_Z_DATA to change the datafile (default $HOME/.z). + Set $_Z_MAX_SCORE lower to age entries out faster (default + 9000). Set $_Z_NO_RESOLVE_SYMLINKS to prevent symlink resolution. Set $_Z_NO_PROMPT_COMMAND to handle PROMPT_COMMAND/precmd your- self. @@ -64,8 +68,8 @@ NOTES Set $_Z_OWNER to allow usage when in 'sudo -s' mode. (These settings should go in .bashrc/.zshrc before the line added above.) - Install the provided man page z.1 somewhere like - /usr/local/man/man1. + Install the provided man page z.1 somewhere in your MANPATH, + like /usr/local/man/man1. Aging: The rank of directories maintained by z undergoes aging based on a sim- @@ -125,7 +129,7 @@ ENVIRONMENT Directories must be full paths without trailing slashes. The environment variable $_Z_OWNER can be set to your username, to - allow usage of z when your sudo enviroment keeps $HOME set. + allow usage of z when your sudo environment keeps $HOME set. FILES Data is stored in $HOME/.z. This can be overridden by setting the diff --git a/z.1 b/z.1 index cc99910..182f981 100644 --- a/z.1 +++ b/z.1 @@ -22,6 +22,9 @@ OPTIONS \fB\-c\fR restrict matches to subdirectories of the current directory .TP +\fB\-e\fR +echo the best match, don't cd +.TP \fB\-h\fR show a brief help message .TP @@ -75,6 +78,9 @@ Set \fB$_Z_CMD\fR to change the command name (default \fBz\fR). Set \fB$_Z_DATA\fR to change the datafile (default \fB$HOME/.z\fR). .RE .RS +Set \fB$_Z_MAX_SCORE\fR lower to age entries out faster (default \fB9000\fR). +.RE +.RS Set \fB$_Z_NO_RESOLVE_SYMLINKS\fR to prevent symlink resolution. .RE .RS @@ -90,7 +96,8 @@ Set \fB$_Z_OWNER\fR to allow usage when in 'sudo -s' mode. (These settings should go in .bashrc/.zshrc before the line added above.) .RE .RS -Install the provided man page \fBz.1\fR somewhere like \fB/usr/local/man/man1\fR. +Install the provided man page \fBz.1\fR somewhere in your \f$MANPATH, like +\fB/usr/local/man/man1\fR. .RE .SS Aging: @@ -151,7 +158,7 @@ directory trees to exclude from tracking. \fB$HOME\fR is always excluded. Directories must be full paths without trailing slashes. .P The environment variable \fB$_Z_OWNER\fR can be set to your username, to -allow usage of \fBz\fR when your sudo enviroment keeps \fB$HOME\fR set. +allow usage of \fBz\fR when your sudo environment keeps \fB$HOME\fR set. .SH FILES Data is stored in \fB$HOME/.z\fR. This can be overridden by setting the diff --git a/z.sh b/z.sh index d0eeb97..fec8d1c 100644 --- a/z.sh +++ b/z.sh @@ -1,4 +1,4 @@ -# Copyright (c) 2009 rupa deadwyler under the WTFPL license +# Copyright (c) 2009 rupa deadwyler. Licensed under the WTFPL license, Version 2 # maintains a jump-list of the directories you actually use # @@ -10,6 +10,7 @@ # * 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_MAX_SCORE lower to age entries out faster (default 9000). # 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. @@ -21,7 +22,10 @@ # * 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 -e foo # echo the best match, don't cd # * z -c foo # restrict matches to subdirs of $PWD +# * z -x # remove the current directory from the datafile +# * z -h # show a brief help message [ -d "${_Z_DATA:-$HOME/.z}" ] && { echo "ERROR: z.sh's datafile (${_Z_DATA:-$HOME/.z}) is a directory." @@ -31,28 +35,42 @@ _z() { local datafile="${_Z_DATA:-$HOME/.z}" + # if symlink, dereference + [ -h "$datafile" ] && datafile=$(readlink "$datafile") + # bail if we don't own ~/.z and $_Z_OWNER not set [ -z "$_Z_OWNER" -a -f "$datafile" -a ! -O "$datafile" ] && return + _z_dirs () { + [ -f "$datafile" ] || return + + local line + while read line; do + # only count directories + [ -d "${line%%\|*}" ] && echo "$line" + done < "$datafile" + return 0 + } + # add entries if [ "$1" = "--add" ]; then shift - # $HOME isn't worth matching - [ "$*" = "$HOME" ] && return + # $HOME and / aren't worth matching + [ "$*" = "$HOME" -o "$*" = '/' ] && return # don't track excluded directory trees - local exclude - for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do - case "$*" in "$exclude*") return;; esac - done + if [ ${#_Z_EXCLUDE_DIRS[@]} -gt 0 ]; then + local exclude + for exclude in "${_Z_EXCLUDE_DIRS[@]}"; do + case "$*" in "$exclude"*) return;; esac + done + fi # 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"|" ' + local score=${_Z_MAX_SCORE:-9000} + _z_dirs | \awk -v path="$*" -v now="$(\date +%s)" -v score=$score -F"|" ' BEGIN { rank[path] = 1 time[path] = now @@ -69,87 +87,84 @@ _z() { count += $2 } END { - if( count > 9000 ) { + if( count > score ) { # 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 + # do our best to avoid clobbering the datafile in a race condition. if [ $? -ne 0 -a -f "$datafile" ]; then - env rm -f "$tempfile" + \env rm -f "$tempfile" else - [ "$_Z_OWNER" ] && chown $_Z_OWNER:$(id -ng $_Z_OWNER) "$tempfile" - env mv -f "$tempfile" "$datafile" || env rm -f "$tempfile" + [ "$_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"|" ' + _z_dirs | \awk -v q="$2" -F"|" ' BEGIN { - if( q == tolower(q) ) imatch = 1 q = substr(q, 3) - gsub(" ", ".*", q) + if( q == tolower(q) ) imatch = 1 + gsub(/ /, ".*", q) } { if( imatch ) { - if( tolower($1) ~ tolower(q) ) print $1 + if( tolower($1) ~ q ) print $1 } else if( $1 ~ q ) print $1 } ' 2>/dev/null else # list/go + local echo fnd last list opt typ 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";; + --) 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;; + h) echo "${_Z_CMD:-z} [-cehlrtx] args" >&2; return;; + l) list=1;; + r) typ="rank";; + t) typ="recent";; + x) \sed -i -e "\:^${PWD}|.*:d" "$datafile";; 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 + *) fnd="$fnd${fnd:+ }$1";; + esac; last=$1; [ "$#" -gt 0 ] && shift; done + [ "$fnd" -a "$fnd" != "^$PWD " ] || 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;; + /*) [ -z "$list" -a -d "$last" ] && builtin 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"|" ' + cd="$( < <( _z_dirs ) \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 + # relate frequency and time + dx = t - time + return int(10000 * rank * (3.75/((0.0001 * dx + 1) + 0.25))) } - function output(files, out, common) { + function output(matches, best_match, common) { # list or return the desired directory if( list ) { - cmd = "sort -n >&2" - for( x in files ) { - if( files[x] ) printf "%-10s %s\n", files[x], x | cmd - } if( common ) { printf "%-10s %s\n", "common:", common > "/dev/stderr" } + cmd = "sort -n >&2" + for( x in matches ) { + if( matches[x] ) { + printf "%-10s %s\n", matches[x], x | cmd + } + } } else { - if( common ) out = common - print out + if( common && !typ ) best_match = common + print best_match } } function common(matches) { @@ -160,11 +175,9 @@ _z() { } } 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 + for( x in matches ) if( matches[x] && index(x, short) != 1 ) { + return + } return short } BEGIN { @@ -192,13 +205,22 @@ _z() { # prefer case sensitive if( best_match ) { output(matches, best_match, common(matches)) + exit } else if( ibest_match ) { output(imatches, ibest_match, common(imatches)) + exit } + exit(1) } ')" - [ $? -gt 0 ] && return - [ "$cd" ] && cd "$cd" + + if [ "$?" -eq 0 ]; then + if [ "$cd" ]; then + if [ "$echo" ]; then echo "$cd"; else builtin cd "$cd"; fi + fi + else + return $? + fi fi } @@ -212,11 +234,13 @@ if type compctl >/dev/null 2>&1; then # populate directory list, avoid clobbering any other precmds. if [ "$_Z_NO_RESOLVE_SYMLINKS" ]; then _z_precmd() { - _z --add "${PWD:a}" + (_z --add "${PWD:a}" &) + : $RANDOM } else _z_precmd() { - _z --add "${PWD:A}" + (_z --add "${PWD:A}" &) + : $RANDOM } fi [[ -n "${precmd_functions[(r)_z_precmd]}" ]] || { @@ -237,7 +261,7 @@ elif type complete >/dev/null 2>&1; then [ "$_Z_NO_PROMPT_COMMAND" ] || { # populate directory list. avoid clobbering other PROMPT_COMMANDs. grep "_z --add" <<< "$PROMPT_COMMAND" >/dev/null || { - PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null;' + PROMPT_COMMAND="$PROMPT_COMMAND"$'\n''(_z --add "$(command pwd '$_Z_RESOLVE_SYMLINKS' 2>/dev/null)" 2>/dev/null &);' } } fi