diff --git a/plugins/scd/README.md b/plugins/scd/README.md index 197cea50..86ab6720 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 1567d273..39b28237 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