diff --git a/plugins/scd/README.md b/plugins/scd/README.md
index 197cea50a..86ab67203 100644
--- a/plugins/scd/README.md
+++ b/plugins/scd/README.md
@@ -11,12 +11,9 @@ the index. A selection menu is displayed in case of several matches, with a
preference given to recently visited paths. `scd` can create permanent
directory aliases, which appear as named directories in zsh session.
-## INSTALLATION
+## INSTALLATION NOTES
-For oh-my-zsh, add `scd` to the `plugins` array in the ~/.zshrc file as in the
-[template file](../../templates/zshrc.zsh-template#L45).
-
-Besides zsh, `scd` can be used with *bash*, *dash* or *tcsh*
+Besides oh-my-zsh, `scd` can be used with *bash*, *dash* or *tcsh*
shells and is also available as [Vim](http://www.vim.org/) plugin and
[IPython](http://ipython.org/) extension. For installation details, see
https://github.com/pavoljuhas/smart-change-directory.
@@ -34,7 +31,7 @@ scd [options] [pattern1 pattern2 ...]
add specified directories to the directory index.
--unindex
- remove specified directories from the index.
+ remove current or specified directories from the index.
-r, --recursive
apply options --add or --unindex recursively.
@@ -47,6 +44,10 @@ scd [options] [pattern1 pattern2 ...]
remove ALIAS definition for the current or specified directory from
~/.scdalias.zsh.
+-A, --all
+ include all matching directories. Disregard matching by directory
+ alias and filtering of less likely paths.
+
--list
show matching directories and exit.
@@ -70,7 +71,7 @@ scd doc
scd a b c
# Change to a directory path that ends with "ts"
-scd "ts(#e)"
+scd "ts$"
# Show selection menu and ranking of 20 most likely directories
scd -v
diff --git a/plugins/scd/scd b/plugins/scd/scd
index 1567d2736..39b28237d 100755
--- a/plugins/scd/scd
+++ b/plugins/scd/scd
@@ -11,20 +11,22 @@ fi
local DOC='scd -- smart change to a recently used directory
usage: scd [options] [pattern1 pattern2 ...]
Go to a directory path that contains all fixed string patterns. Prefer
-recently visited directories and directories with patterns in their tail
-component. Display a selection menu in case of multiple matches.
+recent or frequently visited directories as found in the directory index.
+Display a selection menu in case of multiple matches.
Options:
- -a, --add add specified directories to the directory index
- --unindex remove specified directories from the index
- -r, --recursive apply options --add or --unindex recursively
+ -a, --add add specified directories to the directory index.
+ --unindex remove current or specified directories from the index.
+ -r, --recursive apply options --add or --unindex recursively.
--alias=ALIAS create alias for the current or specified directory and
- store it in ~/.scdalias.zsh
+ store it in ~/.scdalias.zsh.
--unalias remove ALIAS definition for the current or specified
- directory from ~/.scdalias.zsh
- --list show matching directories and exit
- -v, --verbose display directory rank in the selection menu
- -h, --help display this message and exit
+ directory from ~/.scdalias.zsh.
+ -A, --all include all matching directories. Disregard matching by
+ directory alias and filtering of less likely paths.
+ --list show matching directories and exit.
+ -v, --verbose display directory rank in the selection menu.
+ -h, --help display this message and exit.
'
local SCD_HISTFILE=${SCD_HISTFILE:-${HOME}/.scdhistory}
@@ -35,9 +37,9 @@ local SCD_THRESHOLD=${SCD_THRESHOLD:-0.005}
local SCD_SCRIPT=${RUNNING_AS_COMMAND:+$SCD_SCRIPT}
local SCD_ALIAS=~/.scdalias.zsh
-local ICASE a d m p i tdir maxrank threshold
+local ICASE a d m p i maxrank threshold
local opt_help opt_add opt_unindex opt_recursive opt_verbose
-local opt_alias opt_unalias opt_list
+local opt_alias opt_unalias opt_all opt_list
local -A drank dalias
local dmatching
local last_directory
@@ -56,7 +58,8 @@ zmodload -i zsh/zutil
zmodload -i zsh/datetime
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 \
+ -alias:=opt_alias -unalias=opt_unalias \
+ A=opt_all -all=opt_all -list=opt_list \
v=opt_verbose -verbose=opt_verbose h=opt_help -help=opt_help \
|| $EXIT $?
@@ -68,6 +71,11 @@ fi
# load directory aliases if they exist
[[ -r $SCD_ALIAS ]] && source $SCD_ALIAS
+# Private internal functions are prefixed with _scd_Y19oug_.
+# Clean them up when the scd function returns.
+setopt localtraps
+trap 'unfunction -m "_scd_Y19oug_*"' EXIT
+
# works faster than the (:a) modifier and is compatible with zsh 4.2.6
_scd_Y19oug_abspath() {
set -A $1 ${(ps:\0:)"$(
@@ -123,11 +131,52 @@ if [[ -n $opt_unalias ]]; then
$EXIT $?
fi
+# The "compress" function collapses repeated directories to
+# one entry with a time stamp that gives equivalent-probability.
+_scd_Y19oug_compress() {
+ awk -v epochseconds=$EPOCHSECONDS -v meanlife=$SCD_MEANLIFE '
+ BEGIN { FS = "[:;]"; }
+ length($0) < 4096 && $2 > 0 {
+ tau = 1.0 * ($2 - epochseconds) / meanlife;
+ if (tau < -6.9078) tau = -6.9078;
+ prob = exp(tau);
+ sub(/^[^;]*;/, "");
+ if (NF) {
+ dlist[last[$0]] = "";
+ dlist[NR] = $0;
+ last[$0] = NR;
+ ptot[$0] += prob;
+ }
+ }
+ END {
+ for (i = 1; i <= NR; ++i) {
+ d = dlist[i];
+ if (d) {
+ ts = log(ptot[d]) * meanlife + epochseconds;
+ printf(": %.0f:0;%s\n", ts, d);
+ }
+ }
+ }
+ ' $*
+}
+
# 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}
+ # compress repeated entries
+ m=( ${(f)"$(_scd_Y19oug_compress $SCD_HISTFILE)"} )
+ # purge non-existent directories
+ m=( ${(f)"$(
+ for a in $m; do
+ if [[ -d ${a#*;} ]]; then print -r -- $a; fi
+ done
+ )"}
+ )
+ # cut old entries if still oversized
+ if [[ $#m -gt $SCD_HISTSIZE ]]; then
+ m=( ${m[-$SCD_HISTSIZE,-1]} )
+ fi
+ print -lr -- $m >| ${SCD_HISTFILE}
fi
# Determine the last recorded directory
@@ -135,7 +184,6 @@ if [[ -s ${SCD_HISTFILE} ]]; then
last_directory=${"$(tail -1 ${SCD_HISTFILE})"#*;}
fi
-# Internal functions are prefixed with "_scd_Y19oug_".
# The "record" function adds its arguments to the directory index.
_scd_Y19oug_record() {
while [[ -n $last_directory && $1 == $last_directory ]]; do
@@ -217,7 +265,7 @@ _scd_Y19oug_action() {
# set global arrays dmatching and drank
_scd_Y19oug_match() {
## single argument that is an existing directory or directory alias
- if [[ $# == 1 ]] && \
+ if [[ -z $opt_all && $# == 1 ]] && \
[[ -d ${d::=$1} || -d ${d::=${nameddirs[$1]}} ]] && [[ -x $d ]];
then
_scd_Y19oug_abspath dmatching $d
@@ -227,6 +275,8 @@ _scd_Y19oug_match() {
# ignore case unless there is an argument with an uppercase letter
[[ "$*" == *[[:upper:]]* ]] || ICASE='(#i)'
+ # support "$" as an anchor for the directory name ending
+ argv=( ${argv/(#m)?[$](#e)/${MATCH[1]}(#e)} )
# 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
@@ -237,10 +287,10 @@ _scd_Y19oug_match() {
BEGIN { FS = "[:;]"; }
length($0) < 4096 && $2 > 0 {
tau = 1.0 * ($2 - epochseconds) / meanlife;
- if (tau < -4.61) tau = -4.61;
- prec = exp(tau);
+ if (tau < -6.9078) tau = -6.9078;
+ prob = exp(tau);
sub(/^[^;]*;/, "");
- if (NF) ptot[$0] += prec;
+ if (NF) ptot[$0] += prob;
}
END { for (di in ptot) { print di; print ptot[di]; } }'
)"}
@@ -249,9 +299,12 @@ _scd_Y19oug_match() {
# filter drank to the entries that match all arguments
for a; do
- p=${ICASE}"*${a}*"
+ p=${ICASE}"*(${a})*"
drank=( ${(kv)drank[(I)${~p}]} )
done
+ # require at least one argument matches the directory name
+ p=${ICASE}"*(${(j:|:)argv})[^/]#"
+ drank=( ${(kv)drank[(I)${~p}]} )
# build a list of matching directories reverse-sorted by their probabilities
dmatching=( ${(f)"$(
@@ -261,26 +314,6 @@ _scd_Y19oug_match() {
)"}
)
- # 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})} )
@@ -302,6 +335,9 @@ _scd_Y19oug_match() {
# discard all directories below the rank threshold
threshold=$(( maxrank * SCD_THRESHOLD ))
+ if [[ -n ${opt_all} ]]; then
+ threshold=0
+ fi
dmatching=( ${^dmatching}(Ne:'(( ${drank[$REPLY]} >= threshold ))':) )
}
@@ -339,6 +375,7 @@ fi
## here we have multiple matches - display selection menu
a=( {a-z} {A-Z} )
+a=( ${a[1,${#dmatching}]} )
p=( )
for i in {1..${#dmatching}}; do
[[ -n ${a[i]} ]] || break