Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
Re: Dynamic directory name function
On Wed, 23 Sep 2015 09:48:21 +0100
Peter Stephenson <p.stephenson@xxxxxxxxxxx> wrote:
> On Tue, 22 Sep 2015 21:07:56 -0700
> Bart Schaefer <schaefer@xxxxxxxxxxxxxxxx> wrote:
> > On Sep 22, 8:42pm, Peter Stephenson wrote:
> > } Subject: Dynamic directory name function
> > }
> > } For almost as long I've been meaning to turn this into a generic
> > } function that you can configure just with a few variables. I've finally
> > } done that, and this has allowed me to add the completion support that
> > } was missing before.
> >
> > I have one immediate suggestion: Use zstyle instead of variables.
>
> I did think about that. But having written the wrapper function, with
> the variables the way they are, everything immediately fell out neatly
> and clearly with associative arrays having a natural meaning. zstyle
> would just obscure the way the hierarchy was linked for no real gain ---
> quite possibly to the point where I couldn't remember how to do it even
> myself and wouldn't actually use it, in which case the whole thing would
> be a waste of time.
However, it *is* very easy to set a zstyle for the top-level variable.
That's already good enough for you to keep all the definitions outside the
wrapper, if you want. (I don't --- I like having them in one place.)
zmodload -i zsh/parameter
zstyle -s ":zdn:${funcstack[2]}:" mapping _zdn_topvar || _zdn_topvar=zdn_top
I've found a couple of bugs. Probably a good idea if I stick this in
the distribution sooner rather than later to propagate fixes easily.
For record, the latest is below.
pws
## zsh_directory_name_generic
#
# This function is useful as a hook function for the zsh_directory_name
# facility. It provides dynamic directory naming in both directions,
# i.e. from name to directory for use in ~[...] and from directory to
# name for use in prompts etc., as well as completion.
#
# The main feature of this function is a path-like syntax,
# combining abbreviations at multiple levels separated by
# ":". As an example, ~[g:p:s] might specify:
# g - The top level directory for your git area. This first component
# has to match, or the function will retrun indicating another
# directory name hook function should be tried.
# p - The name of a project within your git area.
# s - The source area within that project.
# This allows you to collapse references to long hierarchies to a very
# compact form, particularly if the hierarchies are similar across different
# areas of the disk.
#
# Name components may be completed: if a description is shown at the top
# of the list of completions, it includes the path to which previous
# components expand, while the description for an individual completion
# shows the path segment it would add. No additional configuration is
# needed for this as the completion system is aware of the dynamic
# directory name mechanism.
#
# To use it,
# - Define a wrapper function for your specific case. We'll
# assume it's to be autoloaded. This can have any name but we'll
# refer to it as zdn_wrapper. This wrapper function will define
# various variables and then call this function with the same arguments
# that the wrapper function gets. This configuration is described below.
# - Arrange for the wrapper to be run as a zsh_directory_name hook:
# autoload -Uz add-zsh-hook zsh_diretory_name_generic zdn_wrapper
# add-zsh-hook -U zsh_directory_name zdn_wrapper
#
# Configuration:
#
# The wrapper function should define a local associative array zdn_top.
# Alternatively, this can be set with a style. The context for the
# style is ':zdn:<wrapper-name>:' where <wrapper-name> is the function
# calling zsh_directory_name_generic.
#
# The keys in this correspond to the first component of the name. The
# values are matching directories. They may have an optional suffix
# with a slash followed by a colon and the name of a variable in the
# same format to give the next component. (The slash before the colon
# is to disambiguate the case where a colon is needed in the path for a
# drive. There is otherwise no syntax for escaping this, so path
# components whose names start with a colon are not supported.) A
# special component :default: specifies a variable in the form /:var
# (the path section is ignored and so is usually empty) that will be
# used for the next component if no variable is given for the path.
# Variables referred to within zdn_top have the same format as zdn_top.
#
# For example,
#
# local -A zdn_top=(
# g ~/git
# ga ~/alternate/git
# gs /scratch/$USER/git/:second2
# :default: /:second1
# )
#
# This specifies the behaviour of a directory referred to as ~[g:...] or
# ~[ga:...] or ~[gs:...]. Later path components are optional; in that
# case ~[g] expands to ~/git, and so on. gs expands to /scratch/$USER/git
# and uses the associative array second2 to match the second component;
# g and ga use the associative array second1 to match the second component.
#
# When expanding a name to a directory, if the first component is not g
# or ga or gs, it is not an error; the function simply returns 1 so that
# a later hook function can be tried. However, matching the first
# component commits the function, so if a later component does not
# match, an error is printed (though this does not stop later hooks from
# being executed).
#
# For components after the first, a relative path is expected, but note that
# multiple levels may still appear. Here is an example of second1:
#
# local -A second1=(
# p myproject
# s somproject
# os otherproject/subproject/:third
# )
#
# The path as found from zdn_top is extended with the matching directory,
# so ~[g:p] becomes ~/git/myproject. The "/" between is added automatically
# (it's not possible to have a later component modify the name of a directory
# already matched). Only "os" specifies a variable for a third component,
# and there's no :default:, so it's an error to use a name like ~[g:p:x]
# or ~[ga:s:y] because there's nowhere to look up the x or y.
#
# The associative arrays need to be visible within this function; the
# function therefore uses internal variable names beginning _zdn_ in
# order to avoid clashes. Note that the variable "reply" needs to be
# passed back to the shell, so should not be local in the calling function.
#
# The function does not test whether directories assembled by component
# actually exist; this allows the system to work across automounted
# file systems. The error from the command trying to use a non-existent
# directory should be sufficient to indicate the problem.
#
# Here is a full fictitious but usable autoloadable definition of the
# example function defined by the code above. So ~[gs:p:s] expands
# to /scratch/$USER/git/myscratchproject/top/srcdir.
#
# local -A zdn_top=(
# g ~/git
# ga ~/alternate/git
# gs /scratch/$USER/git/:second2
# :default: /:second1
# )
#
# local -A second1=(
# p myproject
# s somproject
# os otherproject/subproject/:third
# )
#
# local -A second2=(
# p myscratchproject
# s somescratchproject
# )
#
# local -A third=(
# s top/srcdir
# d top/documentation
# )
#
# autoload -Uz zsh_directory_name_generic # paranoia in case we forgot
# zsh_directory_name_generic "$@"
emulate -L zsh
setopt extendedglob
local -a match mbegin mend
# The variable containing the top level mapping.
local _zdn_topvar
zmodload -i zsh/parameter
zstyle -s ":zdn:${funcstack[2]}:" mapping _zdn_topvar || _zdn_topvar=zdn_top
if (( ! ${(P)#_zdn_topvar} )); then
print -r -- "$0: $_zdn_topver is not set" >&2
return 1
fi
local _zdn_var=$_zdn_topvar
local -A _zdn_assoc
if [[ $1 = n ]]; then
# Turning a name into a directory.
local _zdn_name=$2
local -a _zdn_words
local _zdn_dir _zdn_cpt
_zdn_words=(${(s.:.)_zdn_name})
while (( ${#_zdn_words} )); do
if [[ -z ${_zdn_var} ]]; then
print -r -- "$0: too many components in directory name \`$_zdn_name'" >&2
return 1
fi
# Subscripting (P)_zdn_var directly seems not to work.
_zdn_assoc=(${(Pkv)_zdn_var})
_zdn_cpt=${_zdn_assoc[${_zdn_words[1]}]}
shift _zdn_words
if [[ -z $_zdn_cpt ]]; then
# If top level component, just try another expansion
if [[ $_zdn_var != $_zdn_top ]]; then
# Committed to this expansion, so report failure.
print -r -- "$0: no expansion for directory name \`$_zdn_name'" >&2
fi
return 1
fi
if [[ $_zdn_cpt = (#b)(*)/:([[:IDENT:]]##) ]]; then
_zdn_cpt=$match[1]
_zdn_var=$match[2]
else
# may be empty
_zdn_var=${${_zdn_assoc[:default:]}##*/:}
fi
_zdn_dir=${_zdn_dir:+$_zdn_dir/}$_zdn_cpt
done
if (( ${#_zdn_dir} )); then
reply=($_zdn_dir)
return 0
fi
elif [[ $1 = d ]]; then
# Turning a directory into a name.
local _zdn_dir=$2
local _zdn_rest=$_zdn_dir
local -a _zdn_cpts
local _zdn_pref _zdn_pref_raw _zdn_matched _zdn_cpt _zdn_name
while [[ -n $_zdn_var && -n $_zdn_rest ]]; do
_zdn_assoc=(${(Pkv)_zdn_var})
# Sorting in descending order will ensure prefixes
# come after longer strings with that perfix, so
# we match more specific directory names preferentially.
_zdn_cpts=(${(Ov)_zdn_assoc})
_zdn_cpt=''
for _zdn_pref_raw in $_zdn_cpts; do
_zdn_pref=${_zdn_pref_raw%/:*}
[[ -z $_zdn_pref ]] && continue
if [[ $_zdn_rest = $_zdn_pref(#b)(/|)(*) ]]; then
_zdn_cpt=${(k)_zdn_assoc[(r)$_zdn_pref_raw]}
# if we matched a /, too, add it...
_zdn_matched+=$_zdn_pref$match[1]
_zdn_rest=$match[2]
break
fi
done
if [[ -n $_zdn_cpt ]]; then
_zdn_name+=${_zdn_name:+${_zdh_name}:}$_zdn_cpt
if [[ ${_zdn_assoc[$_zdn_cpt]} = (#b)*/:([[:IDENT:]]##) ]]; then
_zdn_var=$match[1]
else
_zdn_var=${${_zdn_assoc[:default:]}##*/:}
fi
else
break
fi
done
if [[ -n $_zdn_name ]]; then
# matched something, so report that.
integer _zdn_len=${#_zdn_matched}
[[ $_zdn_matched[-1] = / ]] && (( _zdn_len-- ))
reply=($_zdn_name $_zdn_len)
return 0
fi
# else let someone else have a go.
elif [[ $1 = c ]]; then
# Completion
if [[ -n $SUFFIX ]]; then
_message "Can't complete in the middle of a dynamic directory name"
else
local -a _zdn_cpts
local _zdn_word _zdn_cpt _zdn_desc _zdn_sofar expl
while [[ -n ${_zdn_var} && ${PREFIX} = (#b)([^:]##):* ]]; do
_zdn_word=$match[1]
compset -P '[^:]##:'
_zdn_assoc=(${(Pkv)_zdn_var})
_zdn_cpt=${_zdn_assoc[$_zdn_word]}
# We only complete at the end so must match here
[[ -z $_zdn_cpt ]] && return 1
if [[ $_zdn_cpt = (#b)(*)/:([[:IDENT:]]##) ]]; then
_zdn_cpt=$match[1]
_zdn_var=$match[2]
else
_zdn_var=${${_zdn_assoc[:default:]}##*/:}
fi
_zdn_sofar+=${_zdn_sofar:+${_zdn_sofar}/}$_zdn_cpt
done
if [[ -n $_zdn_var ]]; then
_zdn_assoc=(${(Pkv)_zdn_var})
local -a _zdn_cpts
for _zdn_cpt _zdn_desc in ${(kv)_zdn_assoc}; do
[[ $_zdn_cpt = :* ]] && continue
_zdn_cpts+=(${_zdn_cpt}:${_zdn_desc%/:[[:IDENT:]]##})
done
_describe -t dirnames "directory name under ${_zdn_sofar%%/}" \
_zdn_cpts -S: -r ':]'
return
fi
fi
fi
# Failed
return 1
## end
Messages sorted by:
Reverse Date,
Date,
Thread,
Author