# created PJ 200907XX jakobi@acm.org # copyright: (c) 2009 jakobi@acm.org, GPL v3 or later # archive: http://jakobi.github.com/script-archive-doc/ # http://blogs.sun.com/nico/entry/ksh_functions_galore typeset|grep -e '^typeset -a cdx$' -e '^integer cdx$' > /dev/null || typeset -i cdx=${cdx:-0} typeset|grep -e '^typeset -a cdx_copy$' -e '^integer cdx_copy$' > /dev/null || typeset -i cdx_copy=0 typeset|grep -e '^typeset -a cdsx$' -e '^integer cdsx$' > /dev/null || typeset -i cdsx=${cdsx:-0} # (c) nico # 20090823 PJ 0.1pj jakobi@acm.org -- modifications to run with both # bash and att ksh (NOT PD-KSH!) # cdto sed extension # BUGS: # - consider using <<<$() to remove some of the bash-ism and differences # between shells and thus simplify this script # input redirection and forks in bash and ksh93 # - ... | while ... is run in a fork in bash, while the last pipeline # stage is run in current context for the real ksh (ast ksh93). # - <(command) is supported in both shells, < <(command) however leads # to a syntax error ok ast ksh93 # NOTE that questionable look-alikes like pdksh supports NEITHER SYNTAX # * <<<"$(command)" - a here document with shell word expansion works # in BOTH bash and ksh WITHOUT FORKING THE RIGHT-SIDE COMMAND, and # could be used below to get rid of cdpipe and a few evals and shell # differences # bash-isms # - skipped pushd for bash by default - bash already has a magic DIRSTACK # plus popd/pushd/dirs, lacking only rightd. DIRSTACK supports assigns, # but adding/removing entries requires use of popd/pushd. Do not unset. # - shopts -s extglob required to allow "regular" matches like +(...) # NOTE: local variable semantics differ - !!AVOID!! # - print replaced by echo or printf if unsafe. # - unset array / typeset -a array / array=() is ok for bash, # but ksh seems treats this like a parent-function-local-array. # If you think this confusing, ksh was also confused and crashed # quite often (*). ksh ABSOLUTELY requires set -A var -- at least # once on initialization (sh (AT&T Research) 93s+ 2008-01-31; one # of the culprits is var=(), leading to a delayed crash # # [alias 'setA'='set -A' vs alias 'setA'=':' is a possibility to # remove a few ksh/bash if/then instances. Aliases are expanded in # the caller so this works. With a function, set -A (ksh only) and # var assignments are visible in the caller, however all typesets # are local for both shells [bash dynamically scoped; ksh statically scoped]. # In reality aliases fail with ksh as I already want to use them on # sourcing, as they're only valid in the next round of parsing; # when set from within functions, they fail for both shells when # using in the same 'parse'. Seems that the current workaround is ok (0825)] # ksh issues # - cdlist=() leads to delayed crashes ... - replaced # [ksh has multiple varname=(...) assignment styles, and for a # bare first word performs alias expansion. Looks like this # syntax is meant to mean: paren-assigments considered harmful, # avoid to survive, and retain sanity. Though I cannot trigger # it. Except one more ksh-ish: using just =() the empty list doesn't # delete the previous array contents. Also kind of contrary to # the man page, unless it is meant to read that =() is trying to # turn a previously indexed array into a compound variable when # no assigments are found. Also note that for normal assignments, # a typeset -a is required as it default to creating a hash instead] # a=(b=44 c=45) degenerated to a scalar string assignment in contrast # a=([b]=44) finally associative, silently changing the type (typeset -a # must be inside the parens). a=(.b=44) compount, access with ${a.x}, # while it isn't accessible as a hash, typeset says the compound # is just an assoc. array. So we've two overlapping abuses of the # underlaying hashes, plus a overloaded access method for indexed # arrays using the same syntax. Plus the crashes. # to the manpage # - Nicolas: ksh93 seems almost a step backwards from ksh88 in many # ways. I've gotten odd crashes here and there, and ^Z on a # pipeline where the last command is a function tends not to work. # [PJ - probably if the function is in the parent envionment, there's # no subshell to suspend, unless you'd be crazy enough to hack # threading into a major shell to enable a few new modes of # broken semantics and deadlocks] # TODO: # - [low pri, on next real change; 20100301] # Nicolas: I noticed your comments about differences in local variable # semantics [between bash and ksh]. In ksh functions created with "foo() { ... }" # syntax are dynamically-scoped, while ones created with "function foo { ... }" # syntax are not. Maybe this would help simplify things? # Changes: # - cdinit tries to retain previous list/stack on sourcing # [bash doesn't allow array export :/, so taking the cd list into # a subshell fails :( ] # - cdto rewritten to allow -n no-exec and optional sed substitution # - cdvi added to edit list # - *_copy is now a global (*) # - original ksh cdsv $PWD-only bug fixed # - replaces set -A and some pipes with shell specific functions # A note from Nicolas: # # Oh, also, the cd utils are not all mine. I hacked on a version that I # got from Will Fiveash, also a Sun employee, and he got that from someone # else. I use it every day; I depend on it; it cuts down enormously on # typing, cut-n-paste. cdlibsedopt=-r; # set to -r to use extended regex for sed cdlibpushd="load"; # also load the pushd and friends # example using a pipeline-capable editor for use in cdvi # (e.g. pipe.vim from jakobi.github.com) cdlibpipeeditor=pipe.vim # should be able to read from stdin/write to stdout # and still allow editing (using /dev/tty or X11) # if you invoke a ksh from bash, you need to change $SHELL, # which ksh93 doesn't do by itself. if [ "${SHELL##*bash}" != "$SHELL" ]; then # bash cdlibusebash="bash" shopt -s extglob cdlibpushd="" # these are builtins function cdsetA { # no set -A eval "unset $1; $1=()" } function cdpipe { # bash executes the pipe tail also in a subshell, # but allows an extended command substitution to # cover this use. Which ksh cannot parse eval "$2 < <($1)" } else # ksh cdlibusebash="" function cdsetA { # using var=() with or w/o set -A leads to delayed crashes eval "unset $1; set -A $1" } function cdpipe { eval "$1 | $2" } fi function cdstripnum { printf "%s" "${1#*([ ])+([0-9])+([ ])}" } function cdvi { cdpipe 'cdls | $cdlibpipeeditor -silent' 'cdrf -'; } function cdhelp { cat <<-"EOF" Usage: cdinit [-c] [] Usage: cdcl Initialize directory list and stack. The cd list file may be given as filename or just as the second part of a ~/.cdpath.* file. cdinit attempts to ~/.cdpath.default if no other file has been specified. Usage: cdfind [quiet] Usage: cdgrep [first] Find a directory, by exact match in the cd list. Find matching directories in the cd list. Usage: cdls Usage: cdrf [-a|--append] [|-] Show the cd list. Use this to save your cd list. Read in a cd list with or without leading numbers and replace the current cd list. Usage: cdsv [] (if none given then CWD) Save the given or current directory to the cd list. Usage: cdow Overwrite the given entry in the cd list with the CWD. Usage: cdto [-n] [] ||[+] Lookup a directory, modify it with the optional (sed $cdlibsedopt), and either change/save or print it (-n). Usage: cdrm [|] Remove a directory, or the current directory from the cd list. Usage: cdsort Sort the cd list (lexicographic sort). Usage: cdvi Edit the cd list. A separate DIRSTACK/pushd implementation is loaded with $cdlibpushd: Usage: pushd [] Usage: popd [] Usage: rightd [] (reverse of popd) Usage: dirs (shows dirs in pushd/popd stack) Directory stack, similar to the C-Shell built-ins of similar names. EOF } function cdfind { typeset p dir typeset -i i if [[ $# -lt 1 || -z "$1" ]] then echo "Usage: cdfind [quiet]" 1>&2 return 5 fi i=0 p="$1" [[ "$1" != /* ]] && p="$PWD/$1" for dir in "${cdlist[@]}" do if [[ "$p" = "$dir" ]] then if [[ "$2" != quiet ]] then echo "Found at index $i" return 0 fi return $i fi i=i+1 done return 255 } function cdgrep { typeset i s typeset -i i=0 s=1 if [[ $# -lt 1 || $# -gt 2 || -z "$1" ]] then echo "Usage: cdgrep [first]" 1>&2 return 5 fi while [[ i -lt cdx ]] do if eval "[[ \"${cdlist[i]}\" = *${1}* ]]" then echo "$i ${cdlist[i]}" s=0 [[ "$2" = first ]] && return 0 fi i=i+1 done return $s } function cdshow { typeset found_at cdfind "$PWD" quiet found_at=$? [[ $found_at -eq 255 ]] && found_at='[unsaved]' echo "$found_at $PWD" return 0 } function cdto { typeset i j dir expr nocd typeset -i i if [[ "$1" = -n ]]; then nocd=1; shift; fi if [[ $# -eq 2 ]]; then expr="$1"; shift; fi if [[ $# -ne 1 || -z "$1" ]]; then echo "Usage: cdto [-n] [] ||[+]" 1>&2 return 5 fi dir="$1" if [[ ! -d "$dir" && "$dir" = +([0-9]) && -d "${cdlist[$dir]}" ]]; then dir="${cdlist[$1]}" fi if [[ ! -d "$dir" && "$dir" = \+* ]]; then dir="$(cdgrep "${dir#\+}" first)" dir="$(cdstripnum "$dir")" fi [ ! -d "$dir" ] && return 1 if [[ -n "$expr" ]]; then dir="$(echo "$dir" | sed $cdlibsedopt -e "$expr")" dir="$(cdstripnum "$dir")" fi #[ ! -d "$dir" ] && return 1 if [[ -n "$nocd" ]]; then printf "%s\n" "$dir" return 0 else if cd "$dir"; then # one of the points of ksh crashes when daring to use cdlist=() cdsv cdshow return 0 fi fi echo "Could not cd to $dir" 1>&2 return 1 } function cdls { typeset -i i=0 while [[ i -lt cdx ]] do echo $i ${cdlist[i]} i=i+1 done } function cdsv { typeset dir current typeset -i i if [[ "$1" = -h || "$1" = --help ]] then echo "Usage: cdsv [] (if none given then CWD)" 1>&2 return 5 fi # Look for $PWD in cdlist[] current="" if [[ $# -eq 0 ]] then current="current " set -- "$PWD" fi for dir in "$@" do cdfind "$dir" quiet i=$? if [[ $i -ne 255 ]] then echo "The ${current}directory $dir is already in the cdlist ($i)" 1>&2 continue fi cdlist[cdx]="$dir" cdx=cdx+1 done } # overwrite entry function cdow { typeset -i i if [[ $# -ne 1 || "$1" != +([0-9]) ]]; then echo "Usage: cdow index#(see cdls)" 1>&2 return 5 fi cdfind "$PWD" quiet i=$? if [[ "$1" -gt ${#cdlist[@]} ]] then echo "Index is beyond cdlist end ($1 > ${#cdlist[@]})" 1>&2 return 1 fi if [[ $i -ne 255 ]] then echo "The current directory is already in the cdlist ($i)" return 1 fi cdlist[$1]=$PWD return 0 } function cdrf { typeset f status dir spath usage usage="Usage: cdrf [-a|--append] [|-]" status=1 unset cdlist_copy typeset -a cdlist_copy cdsetA cdlist_copy cdx_copy=0 # Options while [[ $# -gt 0 && "$1" = -?* ]] do case "$1" in -s|--strip) spath=$2 shift ;; -a|--append) cdx_copy=$cdx cdlist_copy=( "${cdlist[@]}" ) ;; *) echo "$usage" return 5 ;; esac shift done # Default to reading stdin [[ $# -eq 0 ]] && set -- - # Process cdlist files for f in "$@" do if [[ "$f" = - ]] then # STDIN while read dir; do dir="$(cdstripnum "$dir")" dir=${dir#$spath} [[ "$dir" != /* ]] && dir="$PWD/$dir" cdlist_copy[cdx_copy]="${dir}" cdx_copy=cdx_copy+1 done status=0 continue fi # Find the cdlist file if [[ ! -r "$f" && ! -f "$HOME/.cdpath.$f" ]] then echo "No such cdpath file $p or $HOME/.cdpath.$f" 1>&2 continue fi [[ ! -r "$f" && -f "$HOME/.cdpath.$f" ]] && f="$HOME/.cdpath.$f" # Read the cdlist file while read dir; do dir="$(cdstripnum "$dir")" dir=${dir#$spath} [[ "$dir" != /* ]] && dir="$PWD/$dir" cdlist_copy[cdx_copy]="${dir}" cdx_copy=cdx_copy+1 done < "$f" status=0 done [[ $status -ne 0 ]] && return $status # Install new cdlist cdcl cdx=cdx_copy; cdlist=( "${cdlist_copy[@]}" ) return 0 } function cdsort { cdpipe 'cdls | sed -e "s/^[0-9]* //" | sort -u' 'cdrf -' cdls } function cdrm { typeset -i i if [[ -n "$1" && "$1" != +([0-9]) ]] then cdfind "${1:-$PWD}" quiet i=$? elif [[ -n "$1" && "$1" = +([0-9]) ]] then i=$1 elif [[ $# -ne 0 ]] then echo "Usage: cdrm |" 1>&2 return 5 else cdfind "${1:-$PWD}" quiet i=$? fi if [[ "$i" -eq 255 ]] && return 1 then cdfind "${1:-$PWD}" fi cdpipe 'cdls | grep -v "^'"$i"' " | sed -e "s/^[0-9]* //"' 'cdrf -' i=$? cdls return $i } cdinit () { if [[ "$1" = -c ]]; then cdcl shift else cdcl0 fi if [[ $# -eq 0 && -f ~/.cdpath.default ]]; then cdrf ~/.cdpath.default elif [[ $# -eq 1 ]]; then cdrf "$1" fi cdls | tail -5 } cdcl () { cdcl1; cdcl1s; } cdcl0 () { [[ $cdx -eq 0 ]] && cdcl1; [[ $cdsx -eq 0 ]] && cdcl1s; } cdcl1 () { cds=0 cdsetA cdlist } cdcl1s () { [ -n "$cdlibpushd" ] && return cdsx=0 cdsetA cdstack } ################################################################## if [ -n "$cdlibpushd" ]; then function rightd { typeset i typeset -i i if [[ -n "$1" && "$1" != +([0-9]) ]] then echo "Usage: rightd []" 1>&2 return 5 fi i=${1:-1} if [[ ${#cdstack[@]} -le $((cdsx+i)) ]] then echo "No directories to the right on the stack" 1>&2 return 1 fi if [[ ! -d "${cdstack[cdsx+i]}" ]] then echo "Can't cd to: ${cdstack[cdsx+i]}" 1>&2 return 2 fi if cd "${cdstack[cdsx+i]}" then cdsx=cdsx+i cdshow else echo "Could not cd to ${cdstack[cdsx+i]}" 1>&2 return 1 fi return 0 } function pushd { if [[ $# -gt 0 && ! -d "$1" ]] then echo "Can't cd to: $1" 1>&2 return 1 fi cdstack[$cdsx]="$PWD" if cd "${1:-.}" then cdsx=cdsx+1 cdstack[cdsx]="$PWD" [[ "$2" = sv || "$2" = save ]] && cdsv cdshow else echo "Could not cd to $1" 1>&2 return 1 fi return 0 } function pushdsv { pushd "$1" save } function popd { if [[ $((cdsx-${1:-1})) -lt 0 || $((${#cdstack[@]} - ${1:-1})) -lt 0 ]] then echo "Empty stack or popping too much" 1>&2 return 1 fi if [[ ! -d "${cdstack[cdsx-${1:-1}]}" ]] then echo "Can't cd to: ${cdstack[cdsx-${1:-1}]}" 1>&2 return 2 fi if cd "${cdstack[cdsx-1]}" then cdsx=cdsx-1 cdshow else echo "Could not popd to ${cdstack[cdsx-1]}" 1>&2 return 1 fi return 0 } function dirs { typeset -i i if [[ ${#cdstack[@]} -eq 0 ]] then echo "Empty stack" 1>&2 return 1 fi i=1 echo -n "${cdstack[0]}" while [[ $i -le $((cdsx)) && $i -le ${#cdstack[@]} ]] do echo -n " ${cdstack[i]}" i=i+1 done if [[ ${#cdstack[@]} -gt $cdsx && $i -lt ${#cdstack[@]} ]] then echo -n " <-> " while [[ $i -lt ${#cdstack[@]} ]] do echo -n " ${cdstack[i]}" i=i+1 done fi echo return 1 } fi ################################################################## cdinit # vim:ft=sh