36630: new function zsh_directory_name_generic

This commit is contained in:
Peter Stephenson 2015-09-25 21:30:34 +01:00
parent 377e2400b7
commit 649d06a8cd
4 changed files with 342 additions and 2 deletions

View file

@ -1,3 +1,8 @@
2015-09-25 Peter Stephenson <p.w.stephenson@ntlworld.com>
* 36630: Doc/Zsh/contrib.yo, Doc/Zsh/manual.yo,
Functions/Chpwd/zsh_directory_name_generic: new helper function.
2015-09-24 Barton E. Schaefer <schaefer@zsh.org>
* 36623: Doc/Zsh/contrib.yo: document bracketed-paste-magic and

View file

@ -12,6 +12,7 @@ such as shell functions, look for comments in the function source files.
startmenu()
menu(Utilities)
menu(Recent Directories)
menu(Other Directory Functions)
menu(Version Control Information)
menu(Prompt Themes)
menu(ZLE Functions)
@ -324,7 +325,7 @@ options tt(-Uz) are appropriate.
)
enditem()
texinode(Recent Directories)(Version Control Information)(Utilities)(User Contributions)
texinode(Recent Directories)(Other Directory Functions)(Utilities)(User Contributions)
cindex(recent directories, maintaining list of)
cindex(directories, maintaining list of recent)
findex(cdr)
@ -585,7 +586,189 @@ avoid side effects if the change to the directory is to be invisible at the
command line. See the contents of the function tt(chpwd_recent_dirs) for
more details.
texinode(Version Control Information)(Prompt Themes)(Recent Directories)(User Contributions)
texinode(Other Directory Functions)(Version Control Information)(Recent Directories)(User Contributions)
cindex(directories, named, dynamic, helper function)
cindex(dynamic directory naming, helper function)
cindex(named directories, dynamic, helper function)
findex(zsh_directory_name_generic)
sect(Abbreviated dynamic references to directories)
The dynamic directory naming system is described in the subsection
em(Dynamic named directories) of
ifzman(the section em(Filename Expansion) in zmanref(expn))\
ifnzman(noderef(Filename Expansion)). In this, a reference to
tt(~[)var(...)tt(]) is expanded by a function found by the hooks
mechanism.
The contributed function tt(zsh_directory_name_generic) provides a
system allowing the user to refer to directories with only a limited
amount of new code. It supports all three of the standard interfaces
for directory naming: converting from a name to a directory, converting
in the reverse direction to find a short name, and completion of names.
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:
startitem()
item(tt(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.
)
item(tt(p))(
The name of a project within your git area.
)
item(tt(s))(
The source area within that project.
)
enditem()
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.
subsect(Usage)
To use the function, first 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_mywrapper. 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.
Then arrange for the wrapper to be run as a zsh_directory_name hook:
example(autoload -Uz add-zsh-hook zsh_diretory_name_generic zdn_mywrapper
add-zsh-hook -U zsh_directory_name zdn_mywrapper)
subsect(Configuration)
The wrapper function should define a local associative array zdn_top.
Alternatively, this can be set with a style called tt(mapping). The
context for the style is tt(:zdn:)var(wrapper-name) where
var(wrapper-name) is the function calling zsh_directory_name_generic;
for example:
example(zstyle :zdn:zdn_mywrapper: mapping zdn_mywrapper_top)
The keys in this associative array 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 tt(:default:) specifies a variable in the form
tt(/:)var(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 tt(zdn_top) have the same format as
tt(zdn_top) itself, but contain relative paths.
For example,
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 tt(~[g:...])
or tt(~[ga:...]) or tt(~[gs:...]). Later path components are optional;
in that case tt(~[g]) expands to tt(~/git), and so on. tt(gs) expands
to tt(/scratch/$USER/git) and uses the associative array tt(second2) to
match the second component; tt(g) and tt(ga) use the associative array
tt(second1) to match the second component.
When expanding a name to a directory, if the first component is not tt(g) or
tt(ga) or tt(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 still 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 tt(second1):
example(local -A second1=(
p myproject
s somproject
os otherproject/subproject/:third
))
The path as found from tt(zdn_top) is extended with the matching
directory, so tt(~[g:p]) becomes tt(~/git/myproject). The slash between
is added automatically (it's not possible to have a later component
modify the name of a directory already matched). Only tt(os) specifies
a variable for a third component, and there's no tt(:default:), so it's
an error to use a name like tt(~[g:p:x]) or tt(~[ga:s:y]) because
there's nowhere to look up the tt(x) or tt(y).
The associative arrays need to be visible within this function; the
generic function therefore uses internal variable names beginning
tt(_zdn_) in order to avoid clashes. Note that the variable tt(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.
subsect(Complete example)
Here is a full fictitious but usable autoloadable definition of the
example function defined by the code above. So tt(~[gs:p:s]) expands
to tt(/scratch/$USER/git/myscratchproject/top/srcdir) (with tt($USER)
also expanded).
example(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 not needed if you did this at initialisation...
autoload -Uz zsh_directory_name_generic
zsh_directory_name_generic "$@)
It is also possible to use global associative arrays, suitably named,
and set the style for the context of your wrapper function to
refer to this. Then your set up code would contain the following:
example(typeset -A zdn_mywrapper_top=(...)
# ... and so on for other associative arrays ...
zstyle ':zdn:zdn_mywrapper:' mapping zdn_mywrapper_top
autoload -Uz add-zsh-hook zsh_directory_name_generic zdn_mywrapper
add-zsh-hook -U zsh_directory_name zdn_mywrapper)
and the function tt(zdn_mywrapper) would contain only the following:
example(zsh_directory_name_generic "$@")
texinode(Version Control Information)(Prompt Themes)(Other Directory Functions)(User Contributions)
sect(Gathering information from version control systems)
cindex(version control utility)

View file

@ -164,6 +164,7 @@ User Contributions
menu(Utilities)
menu(Recent Directories)
menu(Other Directory Functions)
menu(Version Control Information)
menu(Prompt Themes)
menu(ZLE Functions)

View file

@ -0,0 +1,151 @@
## zsh_directory_name_generic
#
# This function is useful as a hook function for the zsh_directory_name
# facility.
#
# See the zsh-contrib manual page for more.
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
typeset -ag reply
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-- ))
typeset -ag reply
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