diff --git a/plugins/scd/README.md b/plugins/scd/README.md
index ea7c72464..197cea50a 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 9e055eadd..1567d2736 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