diff --git a/plugins/scd/README.md b/plugins/scd/README.md index ea7c7246..197cea50 100644 --- a/plugins/scd/README.md +++ b/plugins/scd/README.md @@ -111,8 +111,7 @@ SCD_MEANLIFE
SCD_THRESHOLD
threshold for cumulative directory likelihood. Directories with - lower likelihood are excluded unless they are the only match to - scd patterns. + a lower likelihood compared to the best match are excluded (0.005).
SCD_SCRIPT
diff --git a/plugins/scd/scd b/plugins/scd/scd index 9e055ead..1567d273 100755 --- a/plugins/scd/scd +++ b/plugins/scd/scd @@ -1,10 +1,11 @@ #!/bin/zsh -f emulate -L zsh +local EXIT=return if [[ $(whence -w $0) == *:' 'command ]]; then emulate -R zsh - alias return=exit local RUNNING_AS_COMMAND=1 + EXIT=exit fi 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 opt_help opt_add opt_unindex opt_recursive opt_verbose local opt_alias opt_unalias opt_list -local -A drank dalias dkey +local -A drank dalias local dmatching +local last_directory 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 \ -alias:=opt_alias -unalias=opt_unalias -list=opt_list \ v=opt_verbose -verbose=opt_verbose h=opt_help -help=opt_help \ - || return $? + || $EXIT $? if [[ -n $opt_help ]]; then print $DOC - return + $EXIT fi # load directory aliases if they exist @@ -79,8 +81,8 @@ _scd_Y19oug_abspath() { # define directory alias if [[ -n $opt_alias ]]; then if [[ -n $1 && ! -d $1 ]]; then - print -u2 "'$1' is not a directory" - return 1 + print -u2 "'$1' is not a directory." + $EXIT 1 fi a=${opt_alias[-1]#=} _scd_Y19oug_abspath d ${1:-$PWD} @@ -93,19 +95,19 @@ if [[ -n $opt_alias ]]; then hash -d -- $a=$d hash -dL >| $SCD_ALIAS ) - return $? + $EXIT $? fi # undefine directory alias if [[ -n $opt_unalias ]]; then if [[ -n $1 && ! -d $1 ]]; then - print -u2 "'$1' is not a directory" - return 1 + print -u2 "'$1' is not a directory." + $EXIT 1 fi _scd_Y19oug_abspath a ${1:-$PWD} a=$(print -rD ${a}) if [[ $a != [~][^/]## ]]; then - return + $EXIT fi a=${a#[~]} # unalias in the current shell, update alias file if successful @@ -118,35 +120,39 @@ if [[ -n $opt_unalias ]]; then hash -dL >| $SCD_ALIAS ) fi - return $? + $EXIT $? 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 ]] && \ (( $(wc -l <$SCD_HISTFILE) > 1.2 * $SCD_HISTSIZE )); then m=( ${(f)"$(<$SCD_HISTFILE)"} ) print -lr -- ${m[-$SCD_HISTSIZE,-1]} >| ${SCD_HISTFILE} 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_". -# The "record" function adds a non-repeating directory to the history -# and turns on history writing. +# The "record" function adds its arguments to the directory index. _scd_Y19oug_record() { - while [[ -n $1 && $1 == ${history[$HISTCMD]} ]]; do + while [[ -n $last_directory && $1 == $last_directory ]]; do shift done - if [[ $# != 0 ]]; then - ( umask 077; : >>| $SCD_HISTFILE ) - p=": ${EPOCHSECONDS}:0;" - print -lr -- ${p}${^*} >> $SCD_HISTFILE + if [[ $# -gt 0 ]]; then + ( umask 077 + p=": ${EPOCHSECONDS}:0;" + print -lr -- ${p}${^*} >>| $SCD_HISTFILE ) fi } if [[ -n $opt_add ]]; then - for a; do - if [[ ! -d $a ]]; then - print -u 2 "Directory $a does not exist" - return 2 + for d; do + if [[ ! -d $d ]]; then + print -u2 "Directory '$d' does not exist." + $EXIT 2 fi done _scd_Y19oug_abspath m ${*:-$PWD} @@ -158,13 +164,13 @@ if [[ -n $opt_add ]]; then print "[done]" done fi - return + $EXIT fi # take care of removing entries from the directory index if [[ -n $opt_unindex ]]; then if [[ ! -s $SCD_HISTFILE ]]; then - return + $EXIT fi # expand existing directories in the argument list for i in {1..$#}; do @@ -190,161 +196,158 @@ if [[ -n $opt_unindex ]]; then } } { print $0 } - ' $SCD_HISTFILE ${*:-$PWD} )" || return $? + ' $SCD_HISTFILE ${*:-$PWD} )" || $EXIT $? : >| ${SCD_HISTFILE} [[ ${#m} == 0 ]] || print -r -- $m >> ${SCD_HISTFILE} - return + $EXIT fi # The "action" function is called when there is just one target directory. _scd_Y19oug_action() { - if [[ -n $opt_list ]]; then - 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 - print -u2 "Warning: running as command with SCD_SCRIPT undefined." - fi - [[ -n $SCD_SCRIPT ]] && (umask 077; - 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 + cd $1 || return $? + if [[ -z $SCD_SCRIPT && -n $RUNNING_AS_COMMAND ]]; then + print -u2 "Warning: running as command with SCD_SCRIPT undefined." + fi + if [[ -n $SCD_SCRIPT ]]; then + print -r "cd ${(q)1}" >| $SCD_SCRIPT fi } -# handle different argument scenarios ---------------------------------------- - -## single argument that is an existing directory -if [[ $# == 1 && -d $1 && -x $1 ]]; then - _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 -[[ "$*" == *[[:upper:]]* ]] || ICASE='(#i)' - -# 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 - <$SCD_HISTFILE \ - awk -v epochseconds=$EPOCHSECONDS -v meanlife=$SCD_MEANLIFE ' - BEGIN { FS = "[:;]"; } - length($0) < 4096 && $2 > 0 { - tau = 1.0 * ($2 - epochseconds) / meanlife; - if (tau < -4.61) tau = -4.61; - prec = exp(tau); - sub(/^[^;]*;/, ""); - if (NF) ptot[$0] += prec; - } - END { for (di in ptot) { print di; print ptot[di]; } }' - )"} -) -unset "drank[/dev/null]" - -# filter drank to the entries that match all arguments -for a; do - p=${ICASE}"*${a}*" - drank=( ${(kv)drank[(I)${~p}]} ) -done - -# build a list of matching directories reverse-sorted by their probabilities -dmatching=( ${(f)"$( - for d p in ${(kv)drank}; do - print -r -- "$p $d"; - done | sort -grk1 | cut -d ' ' -f 2- - )"} -) - -# if some directory paths match all patterns in order, discard all others -p=${ICASE}"*${(j:*:)argv}*" -m=( ${(M)dmatching:#${~p}} ) -[[ -d ${m[1]} ]] && dmatching=( $m ) -# if some directory names match last pattern, discard all others -p=${ICASE}"*${(j:*:)argv}[^/]#" -m=( ${(M)dmatching:#${~p}} ) -[[ -d ${m[1]} ]] && dmatching=( $m ) -# if some directory names match all patterns, discard all others -m=( $dmatching ) -for a; do - p=${ICASE}"*/[^/]#${a}[^/]#" - m=( ${(M)m:#${~p}} ) -done -[[ -d ${m[1]} ]] && dmatching=( $m ) -# if some directory names match all patterns in order, discard all others -p=${ICASE}"/*${(j:[^/]#:)argv}[^/]#" -m=( ${(M)dmatching:#${~p}} ) -[[ -d ${m[1]} ]] && dmatching=( $m ) - -# do not match $HOME or $PWD when run without arguments -if [[ $# == 0 ]]; then - dmatching=( ${dmatching:#(${HOME}|${PWD})} ) -fi - -# keep at most SCD_MENUSIZE of matching and valid directories -m=( ) -for d in $dmatching; do - [[ ${#m} == $SCD_MENUSIZE ]] && break - [[ -d $d && -x $d ]] && m+=$d -done -dmatching=( $m ) - -# find the maximum rank -maxrank=0.0 -for d in $dmatching; do - [[ ${drank[$d]} -lt maxrank ]] || maxrank=${drank[$d]} -done - -# discard all directories below the rank threshold -threshold=$(( maxrank * SCD_THRESHOLD )) -dmatching=( ${^dmatching}(Ne:'(( ${drank[$REPLY]} >= threshold ))':) ) - -## process whatever directories that remained -case ${#dmatching} in -(0) - print -u2 "no matching directory" - return 1 - ;; -(1) - _scd_Y19oug_action $dmatching - return $? - ;; -(*) - # build a list of strings to be displayed in the selection menu - m=( ${(f)"$(print -lD ${dmatching})"} ) - if [[ -n $opt_verbose ]]; then - 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} +# 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 - # 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 - dkey[${a[i]}]=${dalias[$m[i]]} - p+="${a[i]}) ${m[i]}" + + # ignore case unless there is an argument with an uppercase letter + [[ "$*" == *[[:upper:]]* ]] || ICASE='(#i)' + + # 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 + <$SCD_HISTFILE \ + awk -v epochseconds=$EPOCHSECONDS -v meanlife=$SCD_MEANLIFE ' + BEGIN { FS = "[:;]"; } + length($0) < 4096 && $2 > 0 { + tau = 1.0 * ($2 - epochseconds) / meanlife; + if (tau < -4.61) tau = -4.61; + prec = exp(tau); + sub(/^[^;]*;/, ""); + if (NF) ptot[$0] += prec; + } + END { for (di in ptot) { print di; print ptot[di]; } }' + )"} + ) + unset "drank[/dev/null]" + + # filter drank to the entries that match all arguments + for a; do + p=${ICASE}"*${a}*" + drank=( ${(kv)drank[(I)${~p}]} ) done - print -c -r -- $p - if read -s -k 1 d && [[ -n ${dkey[$d]} ]]; then - _scd_Y19oug_action ${dkey[$d]} + + # build a list of matching directories reverse-sorted by their probabilities + dmatching=( ${(f)"$( + for d p in ${(kv)drank}; do + print -r -- "$p $d"; + done | sort -grk1 | cut -d ' ' -f 2- + )"} + ) + + # if some directory paths match all patterns in order, discard all others + p=${ICASE}"*${(j:*:)argv}*" + m=( ${(M)dmatching:#${~p}} ) + [[ -d ${m[1]} ]] && dmatching=( $m ) + # if some directory names match last pattern, discard all others + p=${ICASE}"*${(j:*:)argv}[^/]#" + m=( ${(M)dmatching:#${~p}} ) + [[ -d ${m[1]} ]] && dmatching=( $m ) + # if some directory names match all patterns, discard all others + m=( $dmatching ) + for a; do + p=${ICASE}"*/[^/]#${a}[^/]#" + m=( ${(M)m:#${~p}} ) + done + [[ -d ${m[1]} ]] && dmatching=( $m ) + # if some directory names match all patterns in order, discard all others + p=${ICASE}"/*${(j:[^/]#:)argv}[^/]#" + m=( ${(M)dmatching:#${~p}} ) + [[ -d ${m[1]} ]] && dmatching=( $m ) + + # do not match $HOME or $PWD when run without arguments + if [[ $# == 0 ]]; then + dmatching=( ${dmatching:#(${HOME}|${PWD})} ) fi - return $? -esac + + # keep at most SCD_MENUSIZE of matching and valid directories + m=( ) + for d in $dmatching; do + [[ ${#m} == $SCD_MENUSIZE ]] && break + [[ -d $d && -x $d ]] && m+=$d + done + dmatching=( $m ) + + # find the maximum rank + maxrank=0.0 + for d in $dmatching; do + [[ ${drank[$d]} -lt maxrank ]] || maxrank=${drank[$d]} + done + + # discard all directories below the rank threshold + threshold=$(( maxrank * SCD_THRESHOLD )) + dmatching=( ${^dmatching}(Ne:'(( ${drank[$REPLY]} >= threshold ))':) ) +} + +_scd_Y19oug_match $* + +## process whatever directories that remained +if [[ ${#dmatching} == 0 ]]; then + print -u2 "No matching directory." + $EXIT 1 +fi + +## 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 + $EXIT $? +fi + +## here we have multiple matches - display selection menu +a=( {a-z} {A-Z} ) +p=( ) +for i in {1..${#dmatching}}; do + [[ -n ${a[i]} ]] || break + p+="${a[i]}) ${dalias[${dmatching[i]}]}" +done + +print -c -r -- $p + +if read -s -k 1 d && [[ ${i::=${a[(I)$d]}} -gt 0 ]]; then + _scd_Y19oug_action ${dmatching[i]} + $EXIT $? +fi