Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
scd - smart change directory
- X-seq: zsh-workers 28168
- From: Pavol Juhas <pj2192@xxxxxxxxxxxx>
- To: zsh workers <zsh-workers@xxxxxxx>
- Subject: scd - smart change directory
- Date: Sun, 15 Aug 2010 01:53:23 -0400
- List-help: <mailto:zsh-workers-help@zsh.org>
- List-id: Zsh Workers List <zsh-workers.zsh.org>
- List-post: <mailto:zsh-workers@zsh.org>
- Mail-followup-to: zsh workers <zsh-workers@xxxxxxx>
- Mailing-list: contact zsh-workers-help@xxxxxxx; run by ezmlm
Hello,
I have another iteration of the "scd" function for a quick
change to any directory. The function keeps a history of
the visited directories, which serves as an index of the existing
paths. The match is more likely for recently visited directories,
a selection menu is used in case of several matches. The scd
supports quick creation and removal of permanent directory aliases
(stored in ~/.scdalias.zsh).
I would welcome comments regarding this script.
Cheers,
Pavol
------------------------------------------------------------------------
Installation for zsh: copy the scd script to some directory in $fpath
and add the following lines to the .zshrc file:
# Activate scd
autoload scd
# Add chpwd hook to record all visited directories.
# The hook is executed only when scd function exists.
scd_chpwd_hook() { [[ ${+functions[scd]} == 0 ]] || scd --add . }
autoload add-zsh-hook
add-zsh-hook chpwd scd_chpwd_hook
# Optional - load directory aliases created by scd.
# [[ -f ~/.scdalias.zsh ]] && source ~/.scdalias.zsh
Bourne-style shells: copy the scd script to ${HOME}/bin/
and make it executable. Add the following lines to .bashrc
or an equivalent file:
export SCD_SCRIPT=${HOME}/bin/.scd.go
scd() {
command scd "$@" && . $SCD_SCRIPT
}
C Shell (csh): Same as above, but use the following lines in .cshrc:
setenv SCD_SCRIPT ${HOME}/bin/.scd.go
alias scd "scd \!* && source $SCD_SCRIPT"
------------------------------------------------------------------------
Examples:
# Index recursively some paths for a very first run.
# The index gets otherwise built with every cd command.
scd -ar /tmp/
# Change to a directory path matching "doc"
scd doc
# Change to a path matching all of "a", "b" and "c"
scd a b c
# Change to a directory path ending in "in"
scd "in(#e)"
# Show selection menu and ranking of 25 most likely directories
scd -v
# Brief usage info:
scd --help
#!/bin/zsh -if
# $Id: scd 170 2010-08-15 05:19:17Z juhas $
emulate -L zsh
if [[ $(whence -w $0) == *:' 'command ]]; then
emulate -R zsh
alias return=exit
local RUNNING_AS_COMMAND=1
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.
Options:
-a, --add add specified directories to the directory index
-r, --recursive add directoriese recursively for option --add
--alias=ALIAS create alias for the current or specified directory and
store it in ~/.scdalias.zsh
--unalias remove ALIAS definition for the current or specified
directory from ~/.scdalias.zsh
-v, --verbose display directory rank in the selection menu
-h, --help display this message and exit
'
local SCD_HISTFILE=~/.scdhistory
local SCD_HISTSIZE=${SCD_HISTSIZE:-5000}
local SCD_MENUSIZE=${SCD_MENUSIZE:-25}
local SCD_MEANLIFE=${SCD_MEANLIFE:-86400}
local SCD_THRESHOLD=${SCD_THRESHOLD:-0.005}
local SCD_SCRIPT=${SCD_SCRIPT:-}
local SCD_ALIAS=~/.scdalias.zsh
local ICASE a d m p i tdir maxrank threshold
local opt_help opt_add opt_recursive opt_verbose opt_alias opt_unalias
local -A drank dalias
local dmatching
setopt incappendhistory extendedhistory extendedglob noautonamedirs
[[ ${+options[histsavebycopy]} == 1 ]] && setopt nohistsavebycopy
# make sure that any old commands are removed from SCD_SCRIPT
[[ -n "$SCD_SCRIPT" && -s $SCD_SCRIPT ]] && : >| $SCD_SCRIPT
# process command line options
zmodload -i zsh/zutil
zmodload -i zsh/datetime
zparseopts -D -- h=opt_help -help=opt_help \
a=opt_add -add=opt_add \
r=opt_recursive -recursive=opt_recursive \
v=opt_verbose -verbose=opt_verbose \
-alias:=opt_alias -unalias=opt_unalias \
|| return $?
if [[ -n $opt_help ]]; then
print $DOC
return
fi
# load directory aliases if they exist
[[ -r $SCD_ALIAS ]] && source $SCD_ALIAS
# define directory alias
if [[ -n $opt_alias ]]; then
if [[ -n $1 && ! -d $1 ]]; then
print -u2 "'$1' is not a directory"
return 1
fi
a=${opt_alias[-1]#=}
d=$(unfunction -m "*"; cd ${1:-.}; pwd)
# alias in the current shell, update alias file if successful
hash -d -- $a=$d &&
(
umask 077
hash -dr
[[ -r $SCD_ALIAS ]] && source $SCD_ALIAS
hash -d -- $a=$d
hash -dL >| $SCD_ALIAS
)
return $?
fi
# undefine directory alias
if [[ -n $opt_unalias ]]; then
if [[ -n $1 && ! -d $1 ]]; then
print -u2 "'$1' is not a directory"
return 1
fi
a=$(unfunction -m "*"; cd ${1:-.}; print -rP "%~")
if [[ $a != [~][^/]## ]]; then
return 0
fi
a=${a#[~]}
# unalias in the current shell, update alias file if successful
if unhash -d -- $a 2>/dev/null && [[ -r $SCD_ALIAS ]]; then
(
umask 077
hash -dr
source $SCD_ALIAS
unhash -d -- $a 2>/dev/null &&
hash -dL >| $SCD_ALIAS
)
fi
return $?
fi
# define custom history file
fc -a -p $SCD_HISTFILE $SCD_HISTSIZE
if [[ -n $opt_add ]]; then
for a in ${*:-.}; do
if [[ ! -d $a ]]; then
print -u 2 "Directory $a does not exist"
return 2
fi
d=$(unfunction -m "*"; cd $a; pwd)
print -rs -- $d
if [[ -n $opt_recursive ]]; then
print -n "scanning ${d} ... "
for i in ${d}/**/*(-/N); do
print -rs -- $i
done
print "[done]"
fi
done
return
fi
# self destructive action command
scd_action() {
if [[ $# == 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
# update SCD_HISTFILE unless already done in chpwd hook
[[ -N $SCD_HISTFILE ]] || print -rs $PWD
fi
}
trap 'unfunction scd_action' EXIT
# take care of existing directories
if [[ $# == 1 && -d $1 ]]; then
scd_action $1
return $?
# take care of exact aliases
elif [[ $# == 1 ]] && d=${$(hash -d -m "(#s)$1")#${1}=} && [[ -d $d ]]; then
scd_action $d
return $?
fi
[[ "$*" == *[[:upper:]]* ]] || ICASE='(#i)'
# calculate rank for all directories in the history
drank=( ${(f)"$(
tail -${SCD_HISTSIZE} $SCD_HISTFILE |
awk -v epochseconds=$EPOCHSECONDS -v meanlife=$SCD_MEANLIFE '
BEGIN { FS = "[:;]"; }
length($0) < 4096 {
pi = 0.01 + exp(1.0 * ($2 - epochseconds) / meanlife);
sub(/^[^;]*;/, "");
p[$0] += pi;
}
END { for (di in p) { print di; print p[di]; } }'
)"}
)
for a; do
p=${ICASE}"*${a}*"
drank=( ${(kv)drank[(I)${~p}]} )
done
# build matching directories sorted by rank
dmatching=( ${(f)"$( for d p in ${(kv)drank}; do print -r -- "$p $d"; done |
sort -grk1 | cut -d ' ' -f 2- )"} )
# reduce to exact matches
# patterns follow each other
p=${ICASE}"*${(j:*:)argv}*"
m=( ${(M)dmatching:#${~p}} )
[[ -d ${m[1]} ]] && dmatching=( $m )
# last pattern is in the path tail
p=${ICASE}"*${(j:*:)argv}[^/]#"
m=( ${(M)dmatching:#${~p}} )
[[ -d ${m[1]} ]] && dmatching=( $m )
# all patterns are present in the path tail
m=( $dmatching )
for a; do
p=${ICASE}"*/[^/]#${a}[^/]#"
m=( ${(M)m:#${~p}} )
done
[[ -d ${m[1]} ]] && dmatching=( $m )
# all patterns are in the path tail following each other
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
# cut dmatching to $SCD_MENUSIZE existing directories
m=( )
for d in $dmatching; do
[[ ${#m} == $SCD_MENUSIZE ]] && break
[[ -d $d ]] && m+=$d
done
dmatching=( $m )
# find out maximum rank
maxrank=0.0
for d in $dmatching; do
[[ ${drank[$d]} -lt maxrank ]] || maxrank=${drank[$d]}
done
# cut out directories below rank threshold
threshold=$(( maxrank * SCD_THRESHOLD ))
dmatching=( ${^dmatching}(Ne:'(( ${drank[$REPLY]} >= threshold ))':) )
case ${#dmatching} in
(0)
print -u2 "no matching directory"
return 1
;;
(1)
scd_action $dmatching
return $?
;;
(*)
m=( ${(f)"$(unfunction -m "*";
for d in ${dmatching}; do
cd $d
[[ -n $opt_verbose ]] && printf "%.3g " ${drank[$d]}
print -P "%~"
done)"} )
for i in {1..${#m}}; dalias[${m[i]}]=$dmatching[i]
select d in ${m}; do
scd_action ${dalias[$d]}
return $?
done
esac
Messages sorted by:
Reverse Date,
Date,
Thread,
Author