Add scd plugin for smart change of directory.

Synced with the scd-tracker branch
pavoljuhas/oh-my-zsh@2f78243cad.
This commit is contained in:
Pavol Juhas 2014-03-17 19:49:12 -04:00
parent 6952105bfe
commit c1c107cfb9
2 changed files with 174 additions and 172 deletions

View File

@ -111,8 +111,7 @@ SCD_MEANLIFE</dt><dd>
SCD_THRESHOLD</dt><dd> SCD_THRESHOLD</dt><dd>
threshold for cumulative directory likelihood. Directories with threshold for cumulative directory likelihood. Directories with
lower likelihood are excluded unless they are the only match to a lower likelihood compared to the best match are excluded (0.005).
scd patterns.
</dd><dt> </dd><dt>
SCD_SCRIPT</dt><dd> SCD_SCRIPT</dt><dd>

View File

@ -1,10 +1,11 @@
#!/bin/zsh -f #!/bin/zsh -f
emulate -L zsh emulate -L zsh
local EXIT=return
if [[ $(whence -w $0) == *:' 'command ]]; then if [[ $(whence -w $0) == *:' 'command ]]; then
emulate -R zsh emulate -R zsh
alias return=exit
local RUNNING_AS_COMMAND=1 local RUNNING_AS_COMMAND=1
EXIT=exit
fi fi
local DOC='scd -- smart change to a recently used directory local DOC='scd -- smart change to a recently used directory
@ -37,8 +38,9 @@ local SCD_ALIAS=~/.scdalias.zsh
local ICASE a d m p i tdir maxrank threshold local ICASE a d m p i tdir maxrank threshold
local opt_help opt_add opt_unindex opt_recursive opt_verbose local opt_help opt_add opt_unindex opt_recursive opt_verbose
local opt_alias opt_unalias opt_list local opt_alias opt_unalias opt_list
local -A drank dalias dkey local -A drank dalias
local dmatching local dmatching
local last_directory
setopt extendedhistory extendedglob noautonamedirs brace_ccl setopt extendedhistory extendedglob noautonamedirs brace_ccl
@ -56,11 +58,11 @@ zparseopts -D -- a=opt_add -add=opt_add -unindex=opt_unindex \
r=opt_recursive -recursive=opt_recursive \ r=opt_recursive -recursive=opt_recursive \
-alias:=opt_alias -unalias=opt_unalias -list=opt_list \ -alias:=opt_alias -unalias=opt_unalias -list=opt_list \
v=opt_verbose -verbose=opt_verbose h=opt_help -help=opt_help \ v=opt_verbose -verbose=opt_verbose h=opt_help -help=opt_help \
|| return $? || $EXIT $?
if [[ -n $opt_help ]]; then if [[ -n $opt_help ]]; then
print $DOC print $DOC
return $EXIT
fi fi
# load directory aliases if they exist # load directory aliases if they exist
@ -79,8 +81,8 @@ _scd_Y19oug_abspath() {
# define directory alias # define directory alias
if [[ -n $opt_alias ]]; then if [[ -n $opt_alias ]]; then
if [[ -n $1 && ! -d $1 ]]; then if [[ -n $1 && ! -d $1 ]]; then
print -u2 "'$1' is not a directory" print -u2 "'$1' is not a directory."
return 1 $EXIT 1
fi fi
a=${opt_alias[-1]#=} a=${opt_alias[-1]#=}
_scd_Y19oug_abspath d ${1:-$PWD} _scd_Y19oug_abspath d ${1:-$PWD}
@ -93,19 +95,19 @@ if [[ -n $opt_alias ]]; then
hash -d -- $a=$d hash -d -- $a=$d
hash -dL >| $SCD_ALIAS hash -dL >| $SCD_ALIAS
) )
return $? $EXIT $?
fi fi
# undefine directory alias # undefine directory alias
if [[ -n $opt_unalias ]]; then if [[ -n $opt_unalias ]]; then
if [[ -n $1 && ! -d $1 ]]; then if [[ -n $1 && ! -d $1 ]]; then
print -u2 "'$1' is not a directory" print -u2 "'$1' is not a directory."
return 1 $EXIT 1
fi fi
_scd_Y19oug_abspath a ${1:-$PWD} _scd_Y19oug_abspath a ${1:-$PWD}
a=$(print -rD ${a}) a=$(print -rD ${a})
if [[ $a != [~][^/]## ]]; then if [[ $a != [~][^/]## ]]; then
return $EXIT
fi fi
a=${a#[~]} a=${a#[~]}
# unalias in the current shell, update alias file if successful # unalias in the current shell, update alias file if successful
@ -118,35 +120,39 @@ if [[ -n $opt_unalias ]]; then
hash -dL >| $SCD_ALIAS hash -dL >| $SCD_ALIAS
) )
fi fi
return $? $EXIT $?
fi fi
# Rewrite the history file if it is at least 20% oversized # Rewrite directory index if it is at least 20% oversized
if [[ -s $SCD_HISTFILE ]] && \ if [[ -s $SCD_HISTFILE ]] && \
(( $(wc -l <$SCD_HISTFILE) > 1.2 * $SCD_HISTSIZE )); then (( $(wc -l <$SCD_HISTFILE) > 1.2 * $SCD_HISTSIZE )); then
m=( ${(f)"$(<$SCD_HISTFILE)"} ) m=( ${(f)"$(<$SCD_HISTFILE)"} )
print -lr -- ${m[-$SCD_HISTSIZE,-1]} >| ${SCD_HISTFILE} print -lr -- ${m[-$SCD_HISTSIZE,-1]} >| ${SCD_HISTFILE}
fi fi
# Determine the last recorded directory
if [[ -s ${SCD_HISTFILE} ]]; then
last_directory=${"$(tail -1 ${SCD_HISTFILE})"#*;}
fi
# Internal functions are prefixed with "_scd_Y19oug_". # Internal functions are prefixed with "_scd_Y19oug_".
# The "record" function adds a non-repeating directory to the history # The "record" function adds its arguments to the directory index.
# and turns on history writing.
_scd_Y19oug_record() { _scd_Y19oug_record() {
while [[ -n $1 && $1 == ${history[$HISTCMD]} ]]; do while [[ -n $last_directory && $1 == $last_directory ]]; do
shift shift
done done
if [[ $# != 0 ]]; then if [[ $# -gt 0 ]]; then
( umask 077; : >>| $SCD_HISTFILE ) ( umask 077
p=": ${EPOCHSECONDS}:0;" p=": ${EPOCHSECONDS}:0;"
print -lr -- ${p}${^*} >> $SCD_HISTFILE print -lr -- ${p}${^*} >>| $SCD_HISTFILE )
fi fi
} }
if [[ -n $opt_add ]]; then if [[ -n $opt_add ]]; then
for a; do for d; do
if [[ ! -d $a ]]; then if [[ ! -d $d ]]; then
print -u 2 "Directory $a does not exist" print -u2 "Directory '$d' does not exist."
return 2 $EXIT 2
fi fi
done done
_scd_Y19oug_abspath m ${*:-$PWD} _scd_Y19oug_abspath m ${*:-$PWD}
@ -158,13 +164,13 @@ if [[ -n $opt_add ]]; then
print "[done]" print "[done]"
done done
fi fi
return $EXIT
fi fi
# take care of removing entries from the directory index # take care of removing entries from the directory index
if [[ -n $opt_unindex ]]; then if [[ -n $opt_unindex ]]; then
if [[ ! -s $SCD_HISTFILE ]]; then if [[ ! -s $SCD_HISTFILE ]]; then
return $EXIT
fi fi
# expand existing directories in the argument list # expand existing directories in the argument list
for i in {1..$#}; do for i in {1..$#}; do
@ -190,51 +196,41 @@ if [[ -n $opt_unindex ]]; then
} }
} }
{ print $0 } { print $0 }
' $SCD_HISTFILE ${*:-$PWD} )" || return $? ' $SCD_HISTFILE ${*:-$PWD} )" || $EXIT $?
: >| ${SCD_HISTFILE} : >| ${SCD_HISTFILE}
[[ ${#m} == 0 ]] || print -r -- $m >> ${SCD_HISTFILE} [[ ${#m} == 0 ]] || print -r -- $m >> ${SCD_HISTFILE}
return $EXIT
fi fi
# The "action" function is called when there is just one target directory. # The "action" function is called when there is just one target directory.
_scd_Y19oug_action() { _scd_Y19oug_action() {
if [[ -n $opt_list ]]; then cd $1 || return $?
for d; do
a=${(k)dalias[(r)${d}]}
print -r -- "# $a"
print -r -- $d
done
elif [[ $# == 1 ]]; then
if [[ -z $SCD_SCRIPT && -n $RUNNING_AS_COMMAND ]]; then if [[ -z $SCD_SCRIPT && -n $RUNNING_AS_COMMAND ]]; then
print -u2 "Warning: running as command with SCD_SCRIPT undefined." print -u2 "Warning: running as command with SCD_SCRIPT undefined."
fi fi
[[ -n $SCD_SCRIPT ]] && (umask 077; if [[ -n $SCD_SCRIPT ]]; then
print -r "cd ${(q)1}" >| $SCD_SCRIPT) print -r "cd ${(q)1}" >| $SCD_SCRIPT
[[ -N $SCD_HISTFILE ]] && touch -a $SCD_HISTFILE
cd $1
# record the new directory unless already done in some chpwd hook
[[ -N $SCD_HISTFILE ]] || _scd_Y19oug_record $PWD
fi fi
} }
# handle different argument scenarios ---------------------------------------- # Match and rank patterns to the index file
# set global arrays dmatching and drank
_scd_Y19oug_match() {
## single argument that is an existing directory or directory alias
if [[ $# == 1 ]] && \
[[ -d ${d::=$1} || -d ${d::=${nameddirs[$1]}} ]] && [[ -x $d ]];
then
_scd_Y19oug_abspath dmatching $d
drank[${dmatching[1]}]=1
return
fi
## single argument that is an existing directory # ignore case unless there is an argument with an uppercase letter
if [[ $# == 1 && -d $1 && -x $1 ]]; then [[ "$*" == *[[:upper:]]* ]] || ICASE='(#i)'
_scd_Y19oug_action $1
return $?
## single argument that is an alias
elif [[ $# == 1 && -d ${d::=${nameddirs[$1]}} ]]; then
_scd_Y19oug_action $d
return $?
fi
# ignore case unless there is an argument with an uppercase letter # calculate rank of all directories in the SCD_HISTFILE and keep it as drank
[[ "$*" == *[[:upper:]]* ]] || ICASE='(#i)' # include a dummy entry for splitting of an empty string is buggy
[[ -s $SCD_HISTFILE ]] && drank=( ${(f)"$(
# calculate rank of all directories in the SCD_HISTFILE and keep it as drank
# include a dummy entry for splitting of an empty string is buggy
[[ -s $SCD_HISTFILE ]] && drank=( ${(f)"$(
print -l /dev/null -10 print -l /dev/null -10
<$SCD_HISTFILE \ <$SCD_HISTFILE \
awk -v epochseconds=$EPOCHSECONDS -v meanlife=$SCD_MEANLIFE ' awk -v epochseconds=$EPOCHSECONDS -v meanlife=$SCD_MEANLIFE '
@ -248,103 +244,110 @@ fi
} }
END { for (di in ptot) { print di; print ptot[di]; } }' END { for (di in ptot) { print di; print ptot[di]; } }'
)"} )"}
) )
unset "drank[/dev/null]" unset "drank[/dev/null]"
# filter drank to the entries that match all arguments # filter drank to the entries that match all arguments
for a; do for a; do
p=${ICASE}"*${a}*" p=${ICASE}"*${a}*"
drank=( ${(kv)drank[(I)${~p}]} ) drank=( ${(kv)drank[(I)${~p}]} )
done done
# build a list of matching directories reverse-sorted by their probabilities # build a list of matching directories reverse-sorted by their probabilities
dmatching=( ${(f)"$( dmatching=( ${(f)"$(
for d p in ${(kv)drank}; do for d p in ${(kv)drank}; do
print -r -- "$p $d"; print -r -- "$p $d";
done | sort -grk1 | cut -d ' ' -f 2- done | sort -grk1 | cut -d ' ' -f 2-
)"} )"}
) )
# if some directory paths match all patterns in order, discard all others # if some directory paths match all patterns in order, discard all others
p=${ICASE}"*${(j:*:)argv}*" p=${ICASE}"*${(j:*:)argv}*"
m=( ${(M)dmatching:#${~p}} ) m=( ${(M)dmatching:#${~p}} )
[[ -d ${m[1]} ]] && dmatching=( $m ) [[ -d ${m[1]} ]] && dmatching=( $m )
# if some directory names match last pattern, discard all others # if some directory names match last pattern, discard all others
p=${ICASE}"*${(j:*:)argv}[^/]#" p=${ICASE}"*${(j:*:)argv}[^/]#"
m=( ${(M)dmatching:#${~p}} ) m=( ${(M)dmatching:#${~p}} )
[[ -d ${m[1]} ]] && dmatching=( $m ) [[ -d ${m[1]} ]] && dmatching=( $m )
# if some directory names match all patterns, discard all others # if some directory names match all patterns, discard all others
m=( $dmatching ) m=( $dmatching )
for a; do for a; do
p=${ICASE}"*/[^/]#${a}[^/]#" p=${ICASE}"*/[^/]#${a}[^/]#"
m=( ${(M)m:#${~p}} ) m=( ${(M)m:#${~p}} )
done done
[[ -d ${m[1]} ]] && dmatching=( $m ) [[ -d ${m[1]} ]] && dmatching=( $m )
# if some directory names match all patterns in order, discard all others # if some directory names match all patterns in order, discard all others
p=${ICASE}"/*${(j:[^/]#:)argv}[^/]#" p=${ICASE}"/*${(j:[^/]#:)argv}[^/]#"
m=( ${(M)dmatching:#${~p}} ) m=( ${(M)dmatching:#${~p}} )
[[ -d ${m[1]} ]] && dmatching=( $m ) [[ -d ${m[1]} ]] && dmatching=( $m )
# do not match $HOME or $PWD when run without arguments # do not match $HOME or $PWD when run without arguments
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
dmatching=( ${dmatching:#(${HOME}|${PWD})} ) dmatching=( ${dmatching:#(${HOME}|${PWD})} )
fi fi
# keep at most SCD_MENUSIZE of matching and valid directories # keep at most SCD_MENUSIZE of matching and valid directories
m=( ) m=( )
for d in $dmatching; do for d in $dmatching; do
[[ ${#m} == $SCD_MENUSIZE ]] && break [[ ${#m} == $SCD_MENUSIZE ]] && break
[[ -d $d && -x $d ]] && m+=$d [[ -d $d && -x $d ]] && m+=$d
done done
dmatching=( $m ) dmatching=( $m )
# find the maximum rank # find the maximum rank
maxrank=0.0 maxrank=0.0
for d in $dmatching; do for d in $dmatching; do
[[ ${drank[$d]} -lt maxrank ]] || maxrank=${drank[$d]} [[ ${drank[$d]} -lt maxrank ]] || maxrank=${drank[$d]}
done done
# discard all directories below the rank threshold # discard all directories below the rank threshold
threshold=$(( maxrank * SCD_THRESHOLD )) threshold=$(( maxrank * SCD_THRESHOLD ))
dmatching=( ${^dmatching}(Ne:'(( ${drank[$REPLY]} >= threshold ))':) ) dmatching=( ${^dmatching}(Ne:'(( ${drank[$REPLY]} >= threshold ))':) )
}
_scd_Y19oug_match $*
## process whatever directories that remained ## process whatever directories that remained
case ${#dmatching} in if [[ ${#dmatching} == 0 ]]; then
(0) print -u2 "No matching directory."
print -u2 "no matching directory" $EXIT 1
return 1 fi
;;
(1) ## build formatted directory aliases for selection menu or list display
for d in $dmatching; do
if [[ -n ${opt_verbose} ]]; then
dalias[$d]=$(printf "%.3g %s" ${drank[$d]} $d)
else
dalias[$d]=$(print -Dr -- $d)
fi
done
## process the --list option
if [[ -n $opt_list ]]; then
for d in $dmatching; do
print -r -- "# ${dalias[$d]}"
print -r -- $d
done
$EXIT
fi
## process single directory match
if [[ ${#dmatching} == 1 ]]; then
_scd_Y19oug_action $dmatching _scd_Y19oug_action $dmatching
return $? $EXIT $?
;; fi
(*)
# build a list of strings to be displayed in the selection menu ## here we have multiple matches - display selection menu
m=( ${(f)"$(print -lD ${dmatching})"} ) a=( {a-z} {A-Z} )
if [[ -n $opt_verbose ]]; then p=( )
for i in {1..${#dmatching}}; do for i in {1..${#dmatching}}; do
d=${dmatching[i]}
m[i]=$(printf "%.3g %s" ${drank[$d]} $d)
done
fi
# build a map of string names to actual directory paths
for i in {1..${#m}}; dalias[${m[i]}]=${dmatching[i]}
# opt_list - output matching directories and exit
if [[ -n $opt_list ]]; then
_scd_Y19oug_action ${dmatching}
return
fi
# finally use the selection menu to get the answer
a=( {a-z} {A-Z} )
p=( )
for i in {1..${#m}}; do
[[ -n ${a[i]} ]] || break [[ -n ${a[i]} ]] || break
dkey[${a[i]}]=${dalias[$m[i]]} p+="${a[i]}) ${dalias[${dmatching[i]}]}"
p+="${a[i]}) ${m[i]}" done
done
print -c -r -- $p print -c -r -- $p
if read -s -k 1 d && [[ -n ${dkey[$d]} ]]; then
_scd_Y19oug_action ${dkey[$d]} if read -s -k 1 d && [[ ${i::=${a[(I)$d]}} -gt 0 ]]; then
fi _scd_Y19oug_action ${dmatching[i]}
return $? $EXIT $?
esac fi