|
|
|
|
@ -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 |
|
|
|
|
|