zsh-workers
 help / color / mirror / code / Atom feed
* scd - smart change directory
@ 2010-08-15  5:53 Pavol Juhas
  0 siblings, 0 replies; 2+ messages in thread
From: Pavol Juhas @ 2010-08-15  5:53 UTC (permalink / raw)
  To: zsh workers

[-- Attachment #1: Type: text/plain, Size: 2013 bytes --]

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

[-- Attachment #2: scd --]
[-- Type: text/plain, Size: 6948 bytes --]

#!/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

^ permalink raw reply	[flat|nested] 2+ messages in thread

* scd - smart change directory
@ 2010-07-15 16:51 Pavol Juhas
  0 siblings, 0 replies; 2+ messages in thread
From: Pavol Juhas @ 2010-07-15 16:51 UTC (permalink / raw)
  To: zsh workers

[-- Attachment #1: Type: text/plain, Size: 931 bytes --]

Well, I just read the thread on the "cdr" function - seems
that it is indeed hard to come up with anything new.  :)

Anyway, here is the scd code.  A directory ranking is
calculated from the frequency and time of its visits,
which are recorded in the ~/.scdhistory file.
The scd function accepts one or more patterns arguments,
where all patterns must be present in the full path.
Directories that match with their tail name are preferred.
If scd argument is an existing directory or a directory
alias, scd would jump to that path.

scd can be used as autoloadable zsh function

    autoload scd
    scd p1 p2 ...

or as a zsh executable, which writes a "cd" command to a
file given by the $SCD_SCRIPT environment variable.  The later
mode should support the use of scd from other shells or from the
vim editor.

Cheers,

Pavol

PS: The .scdhistory file is loaded with awk to avoid the
    issues with truncated alternate history.

[-- Attachment #2: scd --]
[-- Type: text/plain, Size: 5127 bytes --]

#!/bin/zsh
# $Id: scd 127 2010-07-15 16:48:32Z juhas $

emulate -L zsh

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
  -v, --verbose     display directory rank in the selection menu
  -h, --help        display this message and exit

This function adds a chpwd hook that records all visited directories.
'

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 ICASE a d m p i tnow tdir maxrank threshold
local opt_help opt_recursive opt_verbose opt_add
local -A drank dalias
local dmatching

setopt nohistsavebycopy extendedhistory extendedglob warncreateglobal

# self destructive action command
scd_action() {
    if [[ $# == 1 ]]; then
        [[ -n $SCD_SCRIPT ]] && (umask 077;
            print -r "cd ${(q)1}" >| $SCD_SCRIPT)
        cd $1
    fi
    unfunction scd_action
}

# define chpwd hook
if [[ -o interactive && ${+functions[scd_chpwd_hook]} == 0 ]]; then
    scd_chpwd_hook()  { scd --add -- $PWD }
    autoload add-zsh-hook
    add-zsh-hook chpwd scd_chpwd_hook
fi

# process command line options
zmodload -i zsh/zutil
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 || return $?

if [[ -n $opt_help ]]; then
    print $DOC
    return
fi

# define custom history file
fc -a -p $SCD_HISTFILE $SCD_HISTSIZE

if [[ -n $opt_add || -n $opt_recursive ]]; then
    for a in ${*:-.}; do
	if [[ ! -d $a ]]; then
	    print -u 2 "Directory $a does not exist"
	    return 2
	fi
	d=$(unfunction -m scd_chpwd_hook; cd $a; pwd)
	print -rs -- $d
	if [[ -n $opt_recursive ]]; then
	    print -n "scanning ${d} ... "
	    m=( ${d}/**/*(/N) )
	    [[ ${#m} == 0 ]] || print -rsl -- $m
	    print "[done]"
	fi
    done
    return
fi

# wipe out the script file
[[ -n $SCD_SCRIPT ]] && /bin/rm -f -- $SCD_SCRIPT

# 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
tnow=${(%):-"%D{%s}"}
drank=( ${(f)"$(
    tail -${SCD_HISTSIZE} $SCD_HISTFILE |
    awk -v tnow=$tnow -v meanlife=$SCD_MEANLIFE '
        BEGIN { FS = "[:;]"; }
        {   pi = 0.01 + exp(1.0 * ($2 - tnow) / 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
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=( ${(f)"$(
    for d in ${dmatching}; do
        (( ${drank[$d]} > threshold )) && print -r -- $d
    done)"}
)
dmatching=( $dmatching )

case ${#dmatching} in
(0)
    print -u2 "no matching directory"
    return 1
    ;;
(1)
    scd_action $dmatching
    return $?
    ;;
(*)
    m=( ${(f)"$(unfunction -m scd_chpwd_hook;
            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

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2010-08-15  6:20 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2010-08-15  5:53 scd - smart change directory Pavol Juhas
  -- strict thread matches above, loose matches on Subject: below --
2010-07-15 16:51 Pavol Juhas

Code repositories for project(s) associated with this public inbox

	https://git.vuxu.org/mirror/zsh/

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).